Skip to content

Commit a92e7df

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 45024a5 commit a92e7df

File tree

13 files changed

+219
-52
lines changed

13 files changed

+219
-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: 68 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,93 @@ - (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.attributedTitle = [[NSAttributedString alloc]
145+
initWithString:@"Dismiss ✕"
146+
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12.0 weight:UIFontWeightRegular]}];
147+
buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12);
148+
buttonConfig.background.backgroundColor = darkerBackground;
149+
buttonConfig.background.cornerRadius = 10;
150+
buttonConfig.baseForegroundColor = color;
151+
152+
self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:nil];
153+
self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
154+
[self->_dismissButton addTarget:self action:@selector(hide) forControlEvents:UIControlEventTouchUpInside];
155+
}
156+
127157
self->_label = [[UILabel alloc] init];
128158
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
129159
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
130160
self->_label.textAlignment = NSTextAlignmentCenter;
131161
self->_label.textColor = color;
132162
self->_label.text = message;
163+
self->_label.numberOfLines = 0;
133164

134165
[self->_window.rootViewController.view addSubview:self->_container];
166+
if (dismissButton) {
167+
[self->_container addSubview:self->_dismissButton];
168+
}
135169
[self->_container addSubview:self->_label];
136170

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

143174
[self->_window layoutIfNeeded];
144175

145-
[NSLayoutConstraint activateConstraints:@[
176+
NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[
146177
// Container constraints
147178
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
148179
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
149180
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
150-
[self->_container.heightAnchor constraintEqualToConstant:height],
151181

152182
// Label constraints
153-
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
183+
[self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 5],
184+
[self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10],
154185
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
155186
]];
187+
188+
// Add button-specific constraints if button exists
189+
if (dismissButton) {
190+
[constraints addObjectsFromArray:@[
191+
[self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10],
192+
[self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor],
193+
[self->_dismissButton.heightAnchor constraintEqualToConstant:22],
194+
[self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10],
195+
]];
196+
} else {
197+
[constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor
198+
constant:-10]];
199+
}
200+
201+
[NSLayoutConstraint activateConstraints:constraints];
156202
});
157203
}
158204

159205
RCT_EXPORT_METHOD(
160206
showMessage : (NSString *)message withColor : (NSNumber *__nonnull)color withBackgroundColor : (NSNumber *__nonnull)
161-
backgroundColor)
207+
backgroundColor withDismissButton : (NSNumber *)dismissButton)
162208
{
163-
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
209+
[self showMessage:message
210+
color:[RCTConvert UIColor:color]
211+
backgroundColor:[RCTConvert UIColor:backgroundColor]
212+
dismissButton:[dismissButton boolValue]];
164213
}
165-
166214
RCT_EXPORT_METHOD(hide)
167215
{
168216
if (!RCTDevLoadingViewGetEnabled()) {
@@ -211,7 +259,7 @@ - (void)showProgressMessage:(NSString *)message
211259
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
212260
}
213261

214-
[self showMessage:message color:color backgroundColor:backgroundColor];
262+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
215263
}
216264

217265
- (void)showOfflineMessage
@@ -225,7 +273,7 @@ - (void)showOfflineMessage
225273
}
226274

227275
NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
228-
[self showMessage:message color:color backgroundColor:backgroundColor];
276+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
229277
}
230278

231279
- (BOOL)isDarkModeEnabled
@@ -284,10 +332,16 @@ + (NSString *)moduleName
284332
+ (void)setEnabled:(BOOL)enabled
285333
{
286334
}
287-
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
335+
- (void)showMessage:(NSString *)message
336+
color:(UIColor *)color
337+
backgroundColor:(UIColor *)backgroundColor
338+
dismissButton:(BOOL)dismissButton
288339
{
289340
}
290-
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
341+
- (void)showMessage:(NSString *)message
342+
withColor:(NSNumber *)color
343+
withBackgroundColor:(NSNumber *)backgroundColor
344+
withDismissButton:(NSNumber *)dismissButton
291345
{
292346
}
293347
- (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: 71 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,70 @@ 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
148+
149+
dismissButtonView.setOnClickListener { hideInternal() }
96150
}
97-
view.setOnClickListener { hideInternal() }
151+
152+
// Allow tapping anywhere on the banner to dismiss
153+
rootView.setOnClickListener { hideInternal() }
154+
98155
val popup =
99156
PopupWindow(
100-
view,
157+
rootView, // Changed from 'view'
101158
ViewGroup.LayoutParams.MATCH_PARENT,
102159
ViewGroup.LayoutParams.WRAP_CONTENT,
103160
)
104161
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
105-
devLoadingView = view
162+
devLoadingView = textView // Store the TextView for updateProgress()
106163
devLoadingPopup = popup
164+
107165
// TODO T164786028: Find out the root cause of the BadTokenException exception here
108166
} catch (e: WindowManager.BadTokenException) {
109167
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)