Skip to content

Commit 04fa474

Browse files
emily8rownfacebook-github-bot
authored andcommitted
Add "Dismiss x" button to the "Disconnected from dev server" Fast Refresh notifier message (#54445)
Summary: Changelog: DevServer fast refresh banners now have a dismiss button but still tap anywhere to dismiss. 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 ec7f729 commit 04fa474

File tree

13 files changed

+270
-44
lines changed

13 files changed

+270
-44
lines changed

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

Lines changed: 9 additions & 2 deletions
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

62-
NativeDevLoadingView.showMessage(
66+
const hasDismissButton = options?.dismissButton ?? false;
67+
68+
NativeDevLoadingView.showMessageV2(
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: 80 additions & 9 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;
@@ -86,6 +87,14 @@ - (void)showInitialMessageDelayed:(void (^)())initialMessage
8687
}
8788

8889
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
90+
{
91+
[self showMessageV2:message color:color backgroundColor:backgroundColor dismissButton:NO];
92+
}
93+
94+
- (void)showMessageV2:(NSString *)message
95+
color:(UIColor *)color
96+
backgroundColor:(UIColor *)backgroundColor
97+
dismissButton:(BOOL)dismissButton
8998
{
9099
if (!RCTDevLoadingViewGetEnabled() || _hiding) {
91100
return;
@@ -120,39 +129,81 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
120129
self->_container = [[UIView alloc] init];
121130
self->_container.backgroundColor = backgroundColor;
122131
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
132+
self->_container.clipsToBounds = YES;
123133
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
124134
[self->_container addGestureRecognizer:tapGesture];
125135
self->_container.userInteractionEnabled = YES;
126136

137+
if (dismissButton) {
138+
CGFloat hue = 0.0;
139+
CGFloat saturation = 0.0;
140+
CGFloat brightness = 0.0;
141+
CGFloat alpha = 0.0;
142+
[backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha];
143+
UIColor *darkerBackground = [UIColor colorWithHue:hue
144+
saturation:saturation
145+
brightness:brightness * 0.7
146+
alpha:1.0];
147+
148+
UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration];
149+
buttonConfig.attributedTitle = [[NSAttributedString alloc]
150+
initWithString:@"Dismiss ✕"
151+
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12.0 weight:UIFontWeightRegular]}];
152+
buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12);
153+
buttonConfig.background.backgroundColor = darkerBackground;
154+
buttonConfig.background.cornerRadius = 10;
155+
buttonConfig.baseForegroundColor = color;
156+
157+
self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:nil];
158+
self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
159+
[self->_dismissButton addTarget:self action:@selector(hide) forControlEvents:UIControlEventTouchUpInside];
160+
}
161+
127162
self->_label = [[UILabel alloc] init];
128163
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
129164
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
130165
self->_label.textAlignment = NSTextAlignmentCenter;
131166
self->_label.textColor = color;
132167
self->_label.text = message;
168+
self->_label.numberOfLines = 0;
133169

134170
[self->_window.rootViewController.view addSubview:self->_container];
171+
if (dismissButton) {
172+
[self->_container addSubview:self->_dismissButton];
173+
}
135174
[self->_container addSubview:self->_label];
136175

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

143179
[self->_window layoutIfNeeded];
144180

145-
[NSLayoutConstraint activateConstraints:@[
181+
NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[
146182
// Container constraints
147183
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
148184
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
149185
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
150-
[self->_container.heightAnchor constraintEqualToConstant:height],
151186

152187
// Label constraints
153-
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
188+
[self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 5],
189+
[self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10],
154190
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
155191
]];
192+
193+
// Add button-specific constraints if button exists
194+
if (dismissButton) {
195+
[constraints addObjectsFromArray:@[
196+
[self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10],
197+
[self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor],
198+
[self->_dismissButton.heightAnchor constraintEqualToConstant:22],
199+
[self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10],
200+
]];
201+
} else {
202+
[constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor
203+
constant:-10]];
204+
}
205+
206+
[NSLayoutConstraint activateConstraints:constraints];
156207
});
157208
}
158209

@@ -162,7 +213,15 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
162213
{
163214
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
164215
}
165-
216+
RCT_EXPORT_METHOD(
217+
showMessageV2 : (NSString *)message withColor : (NSNumber *__nonnull)
218+
color withBackgroundColor : (NSNumber *__nonnull)backgroundColor withDismissButton : (NSNumber *)dismissButton)
219+
{
220+
[self showMessageV2:message
221+
color:[RCTConvert UIColor:color]
222+
backgroundColor:[RCTConvert UIColor:backgroundColor]
223+
dismissButton:[dismissButton boolValue]];
224+
}
166225
RCT_EXPORT_METHOD(hide)
167226
{
168227
if (!RCTDevLoadingViewGetEnabled()) {
@@ -211,7 +270,7 @@ - (void)showProgressMessage:(NSString *)message
211270
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
212271
}
213272

214-
[self showMessage:message color:color backgroundColor:backgroundColor];
273+
[self showMessageV2:message color:color backgroundColor:backgroundColor dismissButton:false];
215274
}
216275

217276
- (void)showOfflineMessage
@@ -225,7 +284,7 @@ - (void)showOfflineMessage
225284
}
226285

227286
NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
228-
[self showMessage:message color:color backgroundColor:backgroundColor];
287+
[self showMessageV2:message color:color backgroundColor:backgroundColor dismissButton:false];
229288
}
230289

231290
- (BOOL)isDarkModeEnabled
@@ -290,6 +349,18 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
290349
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
291350
{
292351
}
352+
- (void)showMessageV2:(NSString *)message
353+
color:(UIColor *)color
354+
backgroundColor:(UIColor *)backgroundColor
355+
dismissButton:(BOOL)dismissButton
356+
{
357+
}
358+
- (void)showMessageV2:(NSString *)message
359+
withColor:(NSNumber *)color
360+
withBackgroundColor:(NSNumber *)backgroundColor
361+
withDismissButton:(NSNumber *)dismissButton
362+
{
363+
}
293364
- (void)showWithURL:(NSURL *)URL
294365
{
295366
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
@protocol RCTDevLoadingViewProtocol <NSObject>
1313
+ (void)setEnabled:(BOOL)enabled;
1414
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor;
15+
- (void)showMessageV2:(NSString *)message
16+
color:(UIColor *)color
17+
backgroundColor:(UIColor *)backgroundColor
18+
dismissButton:(BOOL)dismissButton;
1519
- (void)showWithURL:(NSURL *)URL;
1620
- (void)updateProgress:(RCTLoadingProgress *)progress;
1721
- (void)hide;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
18791879
public fun hide ()V
18801880
public fun showMessage (Ljava/lang/String;)V
18811881
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
1882+
public fun showMessageV2 (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
18821883
public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
18831884
}
18841885

@@ -2131,6 +2132,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevLoad
21312132
public abstract fun hide ()V
21322133
public abstract fun showMessage (Ljava/lang/String;)V
21332134
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
2135+
public abstract fun showMessageV2 (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
21342136
public abstract fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
21352137
}
21362138

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

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

3535
override fun showMessage(message: String) {
36-
showMessage(message, color = null, backgroundColor = null)
36+
showMessageV2(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+
) {
44+
showMessageV2(message, color, backgroundColor, dismissButton = false)
45+
}
46+
47+
override fun showMessageV2(
48+
message: String,
49+
color: Double?,
50+
backgroundColor: Double?,
51+
dismissButton: Boolean?,
52+
) {
4053
if (!isEnabled) {
4154
return
4255
}
43-
UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) }
56+
UiThreadUtil.runOnUiThread {
57+
showInternal(message, color, backgroundColor, dismissButton ?: false)
58+
}
4459
}
4560

4661
override fun updateProgress(status: String?, done: Int?, total: Int?) {
@@ -63,7 +78,12 @@ public class DefaultDevLoadingViewImplementation(
6378
}
6479
}
6580

66-
private fun showInternal(message: String, color: Double?, backgroundColor: Double?) {
81+
private fun showInternal(
82+
message: String,
83+
color: Double?,
84+
backgroundColor: Double?,
85+
dismissButton: Boolean,
86+
) {
6787
if (devLoadingPopup?.isShowing == true) {
6888
// already showing
6989
return
@@ -86,24 +106,70 @@ public class DefaultDevLoadingViewImplementation(
86106
val topOffset = rectangle.top
87107
val inflater =
88108
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())
109+
val rootView = inflater.inflate(R.layout.dev_loading_view, null) as ViewGroup
110+
val textView = rootView.findViewById<TextView>(R.id.loading_text)
111+
textView.text = message
112+
113+
val dismissButtonView = rootView.findViewById<android.widget.Button>(R.id.dismiss_button)
114+
115+
val textLayoutParams = textView.layoutParams as android.widget.RelativeLayout.LayoutParams
116+
if (dismissButton) {
117+
dismissButtonView.visibility = android.view.View.VISIBLE
118+
textLayoutParams.addRule(android.widget.RelativeLayout.END_OF, 0)
119+
textLayoutParams.addRule(android.widget.RelativeLayout.START_OF, R.id.dismiss_button)
120+
} else {
121+
dismissButtonView.visibility = android.view.View.GONE
122+
textLayoutParams.addRule(android.widget.RelativeLayout.START_OF, 0)
123+
textLayoutParams.addRule(
124+
android.widget.RelativeLayout.ALIGN_PARENT_END,
125+
android.widget.RelativeLayout.TRUE,
126+
)
127+
textLayoutParams.addRule(
128+
android.widget.RelativeLayout.ALIGN_PARENT_RIGHT,
129+
android.widget.RelativeLayout.TRUE,
130+
)
93131
}
94-
if (backgroundColor != null) {
95-
view.setBackgroundColor(backgroundColor.toInt())
132+
textView.layoutParams = textLayoutParams
133+
134+
// Use provided colors or defaults (matching iOS behavior)
135+
val textColor = color?.toInt() ?: android.graphics.Color.WHITE
136+
val bgColor =
137+
backgroundColor?.toInt() ?: android.graphics.Color.rgb(64, 64, 64) // Default grey
138+
139+
textView.setTextColor(textColor)
140+
rootView.setBackgroundColor(bgColor)
141+
142+
if (dismissButton) {
143+
dismissButtonView.setTextColor(textColor)
144+
145+
// Darken the background color for the button
146+
val red = (android.graphics.Color.red(bgColor) * 0.7).toInt()
147+
val green = (android.graphics.Color.green(bgColor) * 0.7).toInt()
148+
val blue = (android.graphics.Color.blue(bgColor) * 0.7).toInt()
149+
val darkerColor = android.graphics.Color.rgb(red, green, blue)
150+
151+
// Create rounded drawable for button
152+
val drawable = android.graphics.drawable.GradientDrawable()
153+
drawable.setColor(darkerColor)
154+
drawable.cornerRadius = 15 * rootView.resources.displayMetrics.density
155+
dismissButtonView.background = drawable
156+
157+
dismissButtonView.setOnClickListener { hideInternal() }
96158
}
97-
view.setOnClickListener { hideInternal() }
159+
160+
// Allow tapping anywhere on the banner to dismiss
161+
rootView.setOnClickListener { hideInternal() }
162+
98163
val popup =
99164
PopupWindow(
100-
view,
165+
rootView, // Changed from 'view'
101166
ViewGroup.LayoutParams.MATCH_PARENT,
102167
ViewGroup.LayoutParams.WRAP_CONTENT,
103168
)
104169
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
105-
devLoadingView = view
170+
devLoadingView = textView // Store the TextView for updateProgress()
106171
devLoadingPopup = popup
172+
107173
// TODO T164786028: Find out the root cause of the BadTokenException exception here
108174
} catch (e: WindowManager.BadTokenException) {
109175
FLog.e(

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@ 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+
)
19+
20+
public fun showMessageV2(
21+
message: String,
22+
color: Double?,
23+
backgroundColor: Double?,
24+
dismissButton: Boolean?,
25+
)
1526

1627
public fun updateProgress(status: String?, done: Int?, total: Int?)
1728

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,22 @@ 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+
) {
38+
showMessageV2(message, color, backgroundColor, dismissButton = false)
39+
}
40+
41+
override fun showMessageV2(
42+
message: String,
43+
color: Double?,
44+
backgroundColor: Double?,
45+
dismissButton: Boolean?,
46+
) {
3447
UiThreadUtil.runOnUiThread {
35-
devLoadingViewManager?.showMessage(message, color, backgroundColor)
48+
devLoadingViewManager?.showMessageV2(message, color, backgroundColor, dismissButton)
3649
}
3750
}
3851

0 commit comments

Comments
 (0)