Skip to content

Commit fcd6303

Browse files
emily8rownmeta-codesync[bot]
authored andcommitted
Add "Dismiss x" button to the "Disconnected from dev server" Fast Refresh notifier message (facebook#54445)
Summary: Pull Request resolved: facebook#54445 ## Changelog: [General] [Added] - 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. Reviewed By: cortinico Differential Revision: D86420230 fbshipit-source-id: 2b7cc6601659edb9c38a5ae03b9a3fa4f96e1e95
1 parent 4851fb2 commit fcd6303

File tree

13 files changed

+203
-53
lines changed

13 files changed

+203
-53
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: 70 additions & 15 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;
@@ -124,45 +128,90 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
124128
[self->_container addGestureRecognizer:tapGesture];
125129
self->_container.userInteractionEnabled = YES;
126130

131+
if (dismissButton) {
132+
CGFloat hue = 0.0;
133+
CGFloat saturation = 0.0;
134+
CGFloat brightness = 0.0;
135+
CGFloat alpha = 0.0;
136+
[backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha];
137+
UIColor *darkerBackground = [UIColor colorWithHue:hue
138+
saturation:saturation
139+
brightness:brightness * 0.7
140+
alpha:1.0];
141+
142+
UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration];
143+
buttonConfig.attributedTitle = [[NSAttributedString alloc]
144+
initWithString:@"Dismiss ✕"
145+
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:11.0 weight:UIFontWeightRegular]}];
146+
buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12);
147+
buttonConfig.background.backgroundColor = darkerBackground;
148+
buttonConfig.background.cornerRadius = 10;
149+
buttonConfig.baseForegroundColor = color;
150+
151+
UIAction *dismissAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) {
152+
[self hide];
153+
}];
154+
self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:dismissAction];
155+
self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
156+
}
157+
127158
self->_label = [[UILabel alloc] init];
128159
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
129160
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
130161
self->_label.textAlignment = NSTextAlignmentCenter;
131162
self->_label.textColor = color;
132163
self->_label.text = message;
164+
self->_label.numberOfLines = 0;
133165

134166
[self->_window.rootViewController.view addSubview:self->_container];
167+
if (dismissButton) {
168+
[self->_container addSubview:self->_dismissButton];
169+
}
135170
[self->_container addSubview:self->_label];
136171

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

143175
[self->_window layoutIfNeeded];
144176

145-
[NSLayoutConstraint activateConstraints:@[
177+
NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[
146178
// Container constraints
147179
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
148180
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
149181
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
150-
[self->_container.heightAnchor constraintEqualToConstant:height],
151182

152183
// Label constraints
153-
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
154-
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
184+
[self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 8],
185+
[self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10],
186+
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-8],
155187
]];
188+
189+
// Add button-specific constraints if button exists
190+
if (dismissButton) {
191+
[constraints addObjectsFromArray:@[
192+
[self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10],
193+
[self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor],
194+
[self->_dismissButton.heightAnchor constraintEqualToConstant:22],
195+
[self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10],
196+
]];
197+
} else {
198+
[constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor
199+
constant:-10]];
200+
}
201+
202+
[NSLayoutConstraint activateConstraints:constraints];
156203
});
157204
}
158205

159206
RCT_EXPORT_METHOD(
160207
showMessage : (NSString *)message withColor : (NSNumber *__nonnull)color withBackgroundColor : (NSNumber *__nonnull)
161-
backgroundColor)
208+
backgroundColor withDismissButton : (NSNumber *)dismissButton)
162209
{
163-
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
210+
[self showMessage:message
211+
color:[RCTConvert UIColor:color]
212+
backgroundColor:[RCTConvert UIColor:backgroundColor]
213+
dismissButton:[dismissButton boolValue]];
164214
}
165-
166215
RCT_EXPORT_METHOD(hide)
167216
{
168217
if (!RCTDevLoadingViewGetEnabled()) {
@@ -211,7 +260,7 @@ - (void)showProgressMessage:(NSString *)message
211260
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
212261
}
213262

214-
[self showMessage:message color:color backgroundColor:backgroundColor];
263+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
215264
}
216265

217266
- (void)showOfflineMessage
@@ -225,7 +274,7 @@ - (void)showOfflineMessage
225274
}
226275

227276
NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
228-
[self showMessage:message color:color backgroundColor:backgroundColor];
277+
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
229278
}
230279

231280
- (BOOL)isDarkModeEnabled
@@ -284,10 +333,16 @@ + (NSString *)moduleName
284333
+ (void)setEnabled:(BOOL)enabled
285334
{
286335
}
287-
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
336+
- (void)showMessage:(NSString *)message
337+
color:(UIColor *)color
338+
backgroundColor:(UIColor *)backgroundColor
339+
dismissButton:(BOOL)dismissButton
288340
{
289341
}
290-
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
342+
- (void)showMessage:(NSString *)message
343+
withColor:(NSNumber *)color
344+
withBackgroundColor:(NSNumber *)backgroundColor
345+
withDismissButton:(NSNumber *)dismissButton
291346
{
292347
}
293348
- (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
@@ -1868,7 +1868,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
18681868
public fun <init> (Lcom/facebook/react/devsupport/ReactInstanceDevHelper;)V
18691869
public fun hide ()V
18701870
public fun showMessage (Ljava/lang/String;)V
1871-
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
1871+
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
18721872
public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
18731873
}
18741874

@@ -2120,7 +2120,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevBund
21202120
public abstract interface class com/facebook/react/devsupport/interfaces/DevLoadingViewManager {
21212121
public abstract fun hide ()V
21222122
public abstract fun showMessage (Ljava/lang/String;)V
2123-
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
2123+
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
21242124
public abstract fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
21252125
}
21262126

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

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.facebook.react.devsupport
99

1010
import android.content.Context
11+
import android.graphics.Color
1112
import android.graphics.Rect
1213
import android.view.Gravity
1314
import android.view.LayoutInflater
@@ -33,14 +34,21 @@ public class DefaultDevLoadingViewImplementation(
3334
private var devLoadingPopup: PopupWindow? = null
3435

3536
override fun showMessage(message: String) {
36-
showMessage(message, color = null, backgroundColor = null)
37+
showMessage(message, color = null, backgroundColor = null, dismissButton = false)
3738
}
3839

39-
override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
40+
override fun showMessage(
41+
message: String,
42+
color: Double?,
43+
backgroundColor: Double?,
44+
dismissButton: Boolean?,
45+
) {
4046
if (!isEnabled) {
4147
return
4248
}
43-
UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) }
49+
UiThreadUtil.runOnUiThread {
50+
showInternal(message, color, backgroundColor, dismissButton ?: false)
51+
}
4452
}
4553

4654
override fun updateProgress(status: String?, done: Int?, total: Int?) {
@@ -63,7 +71,12 @@ public class DefaultDevLoadingViewImplementation(
6371
}
6472
}
6573

66-
private fun showInternal(message: String, color: Double?, backgroundColor: Double?) {
74+
private fun showInternal(
75+
message: String,
76+
color: Double?,
77+
backgroundColor: Double?,
78+
dismissButton: Boolean,
79+
) {
6780
if (devLoadingPopup?.isShowing == true) {
6881
// already showing
6982
return
@@ -86,24 +99,56 @@ public class DefaultDevLoadingViewImplementation(
8699
val topOffset = rectangle.top
87100
val inflater =
88101
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())
102+
val rootView = inflater.inflate(R.layout.dev_loading_view, null) as ViewGroup
103+
val textView = rootView.findViewById<TextView>(R.id.loading_text)
104+
textView.text = message
105+
106+
val dismissButtonView = rootView.findViewById<android.widget.Button>(R.id.dismiss_button)
107+
108+
if (dismissButton) {
109+
dismissButtonView.visibility = android.view.View.VISIBLE
110+
} else {
111+
dismissButtonView.visibility = android.view.View.GONE
93112
}
94-
if (backgroundColor != null) {
95-
view.setBackgroundColor(backgroundColor.toInt())
113+
114+
// Use provided colors or defaults (matching iOS behavior)
115+
val textColor = color?.toInt() ?: Color.WHITE
116+
val bgColor = backgroundColor?.toInt() ?: Color.rgb(64, 64, 64) // Default grey
117+
118+
textView.setTextColor(textColor)
119+
rootView.setBackgroundColor(bgColor)
120+
121+
if (dismissButton) {
122+
dismissButtonView.setTextColor(textColor)
123+
124+
// Darken the background color for the button
125+
val red = (Color.red(bgColor) * 0.7).toInt()
126+
val green = (Color.green(bgColor) * 0.7).toInt()
127+
val blue = (Color.blue(bgColor) * 0.7).toInt()
128+
val darkerColor = Color.rgb(red, green, blue)
129+
130+
// Create rounded drawable for button
131+
val drawable = android.graphics.drawable.GradientDrawable()
132+
drawable.setColor(darkerColor)
133+
drawable.cornerRadius = 15 * rootView.resources.displayMetrics.density
134+
dismissButtonView.background = drawable
135+
136+
dismissButtonView.setOnClickListener { hideInternal() }
96137
}
97-
view.setOnClickListener { hideInternal() }
138+
139+
// Allow tapping anywhere on the banner to dismiss
140+
rootView.setOnClickListener { hideInternal() }
141+
98142
val popup =
99143
PopupWindow(
100-
view,
144+
rootView,
101145
ViewGroup.LayoutParams.MATCH_PARENT,
102146
ViewGroup.LayoutParams.WRAP_CONTENT,
103147
)
104148
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
105-
devLoadingView = view
149+
devLoadingView = textView // Store the TextView for updateProgress()
106150
devLoadingPopup = popup
151+
107152
// TODO T164786028: Find out the root cause of the BadTokenException exception here
108153
} catch (e: WindowManager.BadTokenException) {
109154
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)