Skip to content

Commit b75435e

Browse files
emily8rownfacebook-github-bot
authored andcommitted
Add "Dismiss x" button to the "Disconnected from dev server" Fast Refresh notifier message (#54445)
Summary: Add a "Dismiss x" button to the fast refresh disconnected banners in the react native DevLoadingView to act as a visual cue for the current click anywhere to dismiss gesture. Add new parameter to the devLoadingView turbo module to determine whether a message has this dismiss button. Differential Revision: D86420230
1 parent 60471cb commit b75435e

File tree

13 files changed

+213
-52
lines changed

13 files changed

+213
-52
lines changed

packages/react-native/Libraries/Utilities/DevLoadingView.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ const COLOR_SCHEME = {
4444
};
4545

4646
export default {
47-
showMessage(message: string, type: 'load' | 'refresh' | 'error') {
47+
showMessage(
48+
message: string,
49+
type: 'load' | 'refresh' | 'error',
50+
options?: {dismissButton?: boolean},
51+
) {
4852
if (NativeDevLoadingView) {
4953
const colorScheme =
5054
getColorScheme() === 'dark' ? COLOR_SCHEME.dark : COLOR_SCHEME.default;
@@ -59,10 +63,13 @@ export default {
5963
textColor = processColor(colorSet.textColor);
6064
}
6165

66+
const hasDismissButton = options?.dismissButton ?? false;
67+
6268
NativeDevLoadingView.showMessage(
6369
message,
6470
typeof textColor === 'number' ? textColor : null,
6571
typeof backgroundColor === 'number' ? backgroundColor : null,
72+
hasDismissButton,
6673
);
6774
}
6875
},

packages/react-native/Libraries/Utilities/HMRClient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ function setHMRUnavailableReason(reason: string) {
307307
DevLoadingView.showMessage(
308308
'Fast Refresh disconnected. Reload app to reconnect.',
309309
'error',
310+
{dismissButton: true},
310311
);
311312
console.warn(reason);
312313
// (Not using the `warning` module to prevent a Buck cycle.)

packages/react-native/React/CoreModules/RCTDevLoadingView.mm

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ @implementation RCTDevLoadingView {
3232
UIWindow *_window;
3333
UILabel *_label;
3434
UIView *_container;
35+
UIButton *_dismissButton;
3536
NSDate *_showDate;
3637
BOOL _hiding;
3738
dispatch_block_t _initialMessageBlock;
@@ -85,7 +86,10 @@ - (void)showInitialMessageDelayed:(void (^)())initialMessage
8586
dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock);
8687
}
8788

88-
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
89+
- (void)showMessage:(NSString *)message
90+
color:(UIColor *)color
91+
backgroundColor:(UIColor *)backgroundColor
92+
dismissButton:(BOOL)dismissButton
8993
{
9094
if (!RCTDevLoadingViewGetEnabled() || _hiding) {
9195
return;
@@ -120,49 +124,91 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
120124
self->_container = [[UIView alloc] init];
121125
self->_container.backgroundColor = backgroundColor;
122126
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
127+
self->_container.clipsToBounds = YES;
123128
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
124129
[self->_container addGestureRecognizer:tapGesture];
125130
self->_container.userInteractionEnabled = YES;
126131

132+
if (dismissButton) {
133+
CGFloat hue = 0.0;
134+
CGFloat saturation = 0.0;
135+
CGFloat brightness = 0.0;
136+
CGFloat alpha = 0.0;
137+
[backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha];
138+
UIColor *darkerBackground = [UIColor colorWithHue:hue
139+
saturation:saturation
140+
brightness:brightness * 0.7
141+
alpha:1.0];
142+
143+
UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration];
144+
buttonConfig.title = @"Dismiss ✕";
145+
buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(2, 6, 2, 6);
146+
buttonConfig.background.backgroundColor = darkerBackground;
147+
buttonConfig.background.cornerRadius = 10;
148+
buttonConfig.baseForegroundColor = color;
149+
150+
self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:nil];
151+
self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
152+
[self->_dismissButton addTarget:self action:@selector(hide) forControlEvents:UIControlEventTouchUpInside];
153+
}
154+
127155
self->_label = [[UILabel alloc] init];
128156
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
129157
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
130158
self->_label.textAlignment = NSTextAlignmentCenter;
131159
self->_label.textColor = color;
132160
self->_label.text = message;
161+
self->_label.numberOfLines = 0;
133162

134163
[self->_window.rootViewController.view addSubview:self->_container];
164+
if (dismissButton) {
165+
[self->_container addSubview:self->_dismissButton];
166+
}
135167
[self->_container addSubview:self->_label];
136168

137169
CGFloat topSafeAreaHeight = mainWindow.safeAreaInsets.top;
138-
CGFloat height = topSafeAreaHeight + 25;
139-
self->_window.frame = CGRectMake(0, 0, mainWindow.frame.size.width, height);
140-
141170
self->_window.hidden = NO;
142171

143172
[self->_window layoutIfNeeded];
144173

145-
[NSLayoutConstraint activateConstraints:@[
174+
NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[
146175
// Container constraints
147176
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
148177
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
149178
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
150-
[self->_container.heightAnchor constraintEqualToConstant:height],
151179

152180
// Label constraints
153-
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
181+
[self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 5],
182+
[self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10],
154183
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
155184
]];
185+
186+
// Add button-specific constraints if button exists
187+
if (dismissButton) {
188+
[constraints addObjectsFromArray:@[
189+
[self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10],
190+
[self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor],
191+
[self->_dismissButton.heightAnchor constraintEqualToConstant:22],
192+
[self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10],
193+
]];
194+
} else {
195+
[constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor
196+
constant:-10]];
197+
}
198+
199+
[NSLayoutConstraint activateConstraints:constraints];
156200
});
157201
}
158202

159203
RCT_EXPORT_METHOD(
160204
showMessage : (NSString *)message withColor : (NSNumber *__nonnull)color withBackgroundColor : (NSNumber *__nonnull)
161-
backgroundColor)
205+
backgroundColor withDismissButton : (NSNumber *)dismissButton)
162206
{
163-
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
207+
[self showMessage:message
208+
color:[RCTConvert UIColor:color]
209+
backgroundColor:[RCTConvert UIColor:backgroundColor]
210+
dismissButton:[dismissButton boolValue]];
164211
}
165-
166212
RCT_EXPORT_METHOD(hide)
167213
{
168214
if (!RCTDevLoadingViewGetEnabled()) {
@@ -211,7 +257,7 @@ - (void)showProgressMessage:(NSString *)message
211257
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
212258
}
213259

214-
[self showMessage:message color:color backgroundColor:backgroundColor];
260+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
215261
}
216262

217263
- (void)showOfflineMessage
@@ -225,7 +271,7 @@ - (void)showOfflineMessage
225271
}
226272

227273
NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
228-
[self showMessage:message color:color backgroundColor:backgroundColor];
274+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
229275
}
230276

231277
- (BOOL)isDarkModeEnabled
@@ -284,10 +330,16 @@ + (NSString *)moduleName
284330
+ (void)setEnabled:(BOOL)enabled
285331
{
286332
}
287-
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
333+
- (void)showMessage:(NSString *)message
334+
color:(UIColor *)color
335+
backgroundColor:(UIColor *)backgroundColor
336+
dismissButton:(BOOL)dismissButton
288337
{
289338
}
290-
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
339+
- (void)showMessage:(NSString *)message
340+
withColor:(NSNumber *)color
341+
withBackgroundColor:(NSNumber *)backgroundColor
342+
withDismissButton:(NSNumber *)dismissButton
291343
{
292344
}
293345
- (void)showWithURL:(NSURL *)URL

packages/react-native/React/DevSupport/RCTDevLoadingViewProtocol.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
@protocol RCTDevLoadingViewProtocol <NSObject>
1313
+ (void)setEnabled:(BOOL)enabled;
14-
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor;
14+
- (void)showMessage:(NSString *)message
15+
color:(UIColor *)color
16+
backgroundColor:(UIColor *)backgroundColor
17+
dismissButton:(BOOL)dismissButton;
1518
- (void)showWithURL:(NSURL *)URL;
1619
- (void)updateProgress:(RCTLoadingProgress *)progress;
1720
- (void)hide;

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,7 +1878,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
18781878
public fun <init> (Lcom/facebook/react/devsupport/ReactInstanceDevHelper;)V
18791879
public fun hide ()V
18801880
public fun showMessage (Ljava/lang/String;)V
1881-
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
1881+
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
18821882
public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
18831883
}
18841884

@@ -2130,7 +2130,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevBund
21302130
public abstract interface class com/facebook/react/devsupport/interfaces/DevLoadingViewManager {
21312131
public abstract fun hide ()V
21322132
public abstract fun showMessage (Ljava/lang/String;)V
2133-
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
2133+
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
21342134
public abstract fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
21352135
}
21362136

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,21 @@ public class DefaultDevLoadingViewImplementation(
3333
private var devLoadingPopup: PopupWindow? = null
3434

3535
override fun showMessage(message: String) {
36-
showMessage(message, color = null, backgroundColor = null)
36+
showMessage(message, color = null, backgroundColor = null, dismissButton = false)
3737
}
3838

39-
override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
39+
override fun showMessage(
40+
message: String,
41+
color: Double?,
42+
backgroundColor: Double?,
43+
dismissButton: Boolean?,
44+
) {
4045
if (!isEnabled) {
4146
return
4247
}
43-
UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) }
48+
UiThreadUtil.runOnUiThread {
49+
showInternal(message, color, backgroundColor, dismissButton ?: false)
50+
}
4451
}
4552

4653
override fun updateProgress(status: String?, done: Int?, total: Int?) {
@@ -63,7 +70,12 @@ public class DefaultDevLoadingViewImplementation(
6370
}
6471
}
6572

66-
private fun showInternal(message: String, color: Double?, backgroundColor: Double?) {
73+
private fun showInternal(
74+
message: String,
75+
color: Double?,
76+
backgroundColor: Double?,
77+
dismissButton: Boolean,
78+
) {
6779
if (devLoadingPopup?.isShowing == true) {
6880
// already showing
6981
return
@@ -86,24 +98,68 @@ public class DefaultDevLoadingViewImplementation(
8698
val topOffset = rectangle.top
8799
val inflater =
88100
currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
89-
val view = inflater.inflate(R.layout.dev_loading_view, null) as TextView
90-
view.text = message
91-
if (color != null) {
92-
view.setTextColor(color.toInt())
101+
val rootView = inflater.inflate(R.layout.dev_loading_view, null) as ViewGroup
102+
val textView = rootView.findViewById<TextView>(R.id.loading_text)
103+
textView.text = message
104+
105+
val dismissButtonView = rootView.findViewById<android.widget.Button>(R.id.dismiss_button)
106+
107+
val textLayoutParams = textView.layoutParams as android.widget.RelativeLayout.LayoutParams
108+
if (dismissButton) {
109+
dismissButtonView.visibility = android.view.View.VISIBLE
110+
textLayoutParams.addRule(android.widget.RelativeLayout.END_OF, 0)
111+
textLayoutParams.addRule(android.widget.RelativeLayout.START_OF, R.id.dismiss_button)
112+
} else {
113+
dismissButtonView.visibility = android.view.View.GONE
114+
textLayoutParams.addRule(android.widget.RelativeLayout.START_OF, 0)
115+
textLayoutParams.addRule(
116+
android.widget.RelativeLayout.ALIGN_PARENT_END,
117+
android.widget.RelativeLayout.TRUE,
118+
)
119+
textLayoutParams.addRule(
120+
android.widget.RelativeLayout.ALIGN_PARENT_RIGHT,
121+
android.widget.RelativeLayout.TRUE,
122+
)
93123
}
94-
if (backgroundColor != null) {
95-
view.setBackgroundColor(backgroundColor.toInt())
124+
textView.layoutParams = textLayoutParams
125+
126+
// Use provided colors or defaults (matching iOS behavior)
127+
val textColor = color?.toInt() ?: android.graphics.Color.WHITE
128+
val bgColor =
129+
backgroundColor?.toInt() ?: android.graphics.Color.rgb(64, 64, 64) // Default grey
130+
131+
textView.setTextColor(textColor)
132+
rootView.setBackgroundColor(bgColor)
133+
134+
if (dismissButton) {
135+
dismissButtonView.setTextColor(textColor)
136+
137+
// Darken the background color for the button
138+
val red = (android.graphics.Color.red(bgColor) * 0.7).toInt()
139+
val green = (android.graphics.Color.green(bgColor) * 0.7).toInt()
140+
val blue = (android.graphics.Color.blue(bgColor) * 0.7).toInt()
141+
val darkerColor = android.graphics.Color.rgb(red, green, blue)
142+
143+
// Create rounded drawable for button
144+
val drawable = android.graphics.drawable.GradientDrawable()
145+
drawable.setColor(darkerColor)
146+
drawable.cornerRadius = 15 * rootView.resources.displayMetrics.density
147+
dismissButtonView.background = drawable
96148
}
97-
view.setOnClickListener { hideInternal() }
149+
150+
// Allow tapping anywhere on the banner to dismiss
151+
rootView.setOnClickListener { hideInternal() }
152+
98153
val popup =
99154
PopupWindow(
100-
view,
155+
rootView, // Changed from 'view'
101156
ViewGroup.LayoutParams.MATCH_PARENT,
102157
ViewGroup.LayoutParams.WRAP_CONTENT,
103158
)
104159
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
105-
devLoadingView = view
160+
devLoadingView = textView // Store the TextView for updateProgress()
106161
devLoadingPopup = popup
162+
107163
// TODO T164786028: Find out the root cause of the BadTokenException exception here
108164
} catch (e: WindowManager.BadTokenException) {
109165
FLog.e(

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevLoadingViewManager.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ package com.facebook.react.devsupport.interfaces
1111
public interface DevLoadingViewManager {
1212
public fun showMessage(message: String)
1313

14-
public fun showMessage(message: String, color: Double?, backgroundColor: Double?)
14+
public fun showMessage(
15+
message: String,
16+
color: Double?,
17+
backgroundColor: Double?,
18+
dismissButton: Boolean?,
19+
)
1520

1621
public fun updateProgress(status: String?, done: Int?, total: Int?)
1722

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/devloading/DevLoadingModule.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ internal class DevLoadingModule(reactContext: ReactApplicationContext) :
3030
}
3131
}
3232

33-
override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
33+
override fun showMessage(
34+
message: String,
35+
color: Double?,
36+
backgroundColor: Double?,
37+
dismissButton: Boolean?,
38+
) {
3439
UiThreadUtil.runOnUiThread {
35-
devLoadingViewManager?.showMessage(message, color, backgroundColor)
40+
devLoadingViewManager?.showMessage(message, color, backgroundColor, dismissButton)
3641
}
3742
}
3843

0 commit comments

Comments
 (0)