Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/react-native/Libraries/Utilities/DevLoadingView.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ const COLOR_SCHEME = {
};

export default {
showMessage(message: string, type: 'load' | 'refresh' | 'error') {
showMessage(
message: string,
type: 'load' | 'refresh' | 'error',
options?: {dismissButton?: boolean},
) {
if (NativeDevLoadingView) {
const colorScheme =
getColorScheme() === 'dark' ? COLOR_SCHEME.dark : COLOR_SCHEME.default;
Expand All @@ -59,10 +63,13 @@ export default {
textColor = processColor(colorSet.textColor);
}

const hasDismissButton = options?.dismissButton ?? false;

NativeDevLoadingView.showMessage(
message,
typeof textColor === 'number' ? textColor : null,
typeof backgroundColor === 'number' ? backgroundColor : null,
hasDismissButton,
);
}
},
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/Libraries/Utilities/HMRClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ function setHMRUnavailableReason(reason: string) {
DevLoadingView.showMessage(
'Fast Refresh disconnected. Reload app to reconnect.',
'error',
{dismissButton: true},
);
console.warn(reason);
// (Not using the `warning` module to prevent a Buck cycle.)
Expand Down
84 changes: 69 additions & 15 deletions packages/react-native/React/CoreModules/RCTDevLoadingView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ @implementation RCTDevLoadingView {
UIWindow *_window;
UILabel *_label;
UIView *_container;
UIButton *_dismissButton;
NSDate *_showDate;
BOOL _hiding;
dispatch_block_t _initialMessageBlock;
Expand Down Expand Up @@ -85,7 +86,10 @@ - (void)showInitialMessageDelayed:(void (^)())initialMessage
dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock);
}

- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
- (void)showMessage:(NSString *)message
color:(UIColor *)color
backgroundColor:(UIColor *)backgroundColor
dismissButton:(BOOL)dismissButton
{
if (!RCTDevLoadingViewGetEnabled() || _hiding) {
return;
Expand Down Expand Up @@ -120,49 +124,93 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(
self->_container = [[UIView alloc] init];
self->_container.backgroundColor = backgroundColor;
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
self->_container.clipsToBounds = YES;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
[self->_container addGestureRecognizer:tapGesture];
self->_container.userInteractionEnabled = YES;

if (dismissButton) {
CGFloat hue = 0.0;
CGFloat saturation = 0.0;
CGFloat brightness = 0.0;
CGFloat alpha = 0.0;
[backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha];
UIColor *darkerBackground = [UIColor colorWithHue:hue
saturation:saturation
brightness:brightness * 0.7
alpha:1.0];

UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration];
buttonConfig.attributedTitle = [[NSAttributedString alloc]
initWithString:@"Dismiss ✕"
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:11.0 weight:UIFontWeightRegular]}];
buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12);
buttonConfig.background.backgroundColor = darkerBackground;
buttonConfig.background.cornerRadius = 10;
buttonConfig.baseForegroundColor = color;

self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:nil];
self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
[self->_dismissButton addTarget:self action:@selector(hide) forControlEvents:UIControlEventTouchUpInside];
}

self->_label = [[UILabel alloc] init];
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
self->_label.textAlignment = NSTextAlignmentCenter;
self->_label.textColor = color;
self->_label.text = message;
self->_label.numberOfLines = 0;

[self->_window.rootViewController.view addSubview:self->_container];
if (dismissButton) {
[self->_container addSubview:self->_dismissButton];
}
[self->_container addSubview:self->_label];

CGFloat topSafeAreaHeight = mainWindow.safeAreaInsets.top;
CGFloat height = topSafeAreaHeight + 25;
self->_window.frame = CGRectMake(0, 0, mainWindow.frame.size.width, height);

self->_window.hidden = NO;

[self->_window layoutIfNeeded];

[NSLayoutConstraint activateConstraints:@[
NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[
// Container constraints
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
[self->_container.heightAnchor constraintEqualToConstant:height],

// Label constraints
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
[self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 8],
[self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10],
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-8],
]];

// Add button-specific constraints if button exists
if (dismissButton) {
[constraints addObjectsFromArray:@[
[self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10],
[self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor],
[self->_dismissButton.heightAnchor constraintEqualToConstant:22],
[self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10],
]];
} else {
[constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor
constant:-10]];
}

[NSLayoutConstraint activateConstraints:constraints];
});
}

RCT_EXPORT_METHOD(
showMessage : (NSString *)message withColor : (NSNumber *__nonnull)color withBackgroundColor : (NSNumber *__nonnull)
backgroundColor)
backgroundColor withDismissButton : (NSNumber *)dismissButton)
{
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
[self showMessage:message
color:[RCTConvert UIColor:color]
backgroundColor:[RCTConvert UIColor:backgroundColor]
dismissButton:[dismissButton boolValue]];
}

RCT_EXPORT_METHOD(hide)
{
if (!RCTDevLoadingViewGetEnabled()) {
Expand Down Expand Up @@ -211,7 +259,7 @@ - (void)showProgressMessage:(NSString *)message
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
}

[self showMessage:message color:color backgroundColor:backgroundColor];
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
}

- (void)showOfflineMessage
Expand All @@ -225,7 +273,7 @@ - (void)showOfflineMessage
}

NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
[self showMessage:message color:color backgroundColor:backgroundColor];
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
}

- (BOOL)isDarkModeEnabled
Expand Down Expand Up @@ -284,10 +332,16 @@ + (NSString *)moduleName
+ (void)setEnabled:(BOOL)enabled
{
}
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
- (void)showMessage:(NSString *)message
color:(UIColor *)color
backgroundColor:(UIColor *)backgroundColor
dismissButton:(BOOL)dismissButton
{
}
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
- (void)showMessage:(NSString *)message
withColor:(NSNumber *)color
withBackgroundColor:(NSNumber *)backgroundColor
withDismissButton:(NSNumber *)dismissButton
{
}
- (void)showWithURL:(NSURL *)URL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

@protocol RCTDevLoadingViewProtocol <NSObject>
+ (void)setEnabled:(BOOL)enabled;
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor;
- (void)showMessage:(NSString *)message
color:(UIColor *)color
backgroundColor:(UIColor *)backgroundColor
dismissButton:(BOOL)dismissButton;
- (void)showWithURL:(NSURL *)URL;
- (void)updateProgress:(RCTLoadingProgress *)progress;
- (void)hide;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -1878,7 +1878,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
public fun <init> (Lcom/facebook/react/devsupport/ReactInstanceDevHelper;)V
public fun hide ()V
public fun showMessage (Ljava/lang/String;)V
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
}

Expand Down Expand Up @@ -2130,7 +2130,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevBund
public abstract interface class com/facebook/react/devsupport/interfaces/DevLoadingViewManager {
public abstract fun hide ()V
public abstract fun showMessage (Ljava/lang/String;)V
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)V
public abstract fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.facebook.react.devsupport

import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.view.Gravity
import android.view.LayoutInflater
Expand All @@ -33,14 +34,21 @@ public class DefaultDevLoadingViewImplementation(
private var devLoadingPopup: PopupWindow? = null

override fun showMessage(message: String) {
showMessage(message, color = null, backgroundColor = null)
showMessage(message, color = null, backgroundColor = null, dismissButton = false)
}

override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
override fun showMessage(
message: String,
color: Double?,
backgroundColor: Double?,
dismissButton: Boolean?,
) {
if (!isEnabled) {
return
}
UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) }
UiThreadUtil.runOnUiThread {
showInternal(message, color, backgroundColor, dismissButton ?: false)
}
}

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

private fun showInternal(message: String, color: Double?, backgroundColor: Double?) {
private fun showInternal(
message: String,
color: Double?,
backgroundColor: Double?,
dismissButton: Boolean,
) {
if (devLoadingPopup?.isShowing == true) {
// already showing
return
Expand All @@ -86,24 +99,56 @@ public class DefaultDevLoadingViewImplementation(
val topOffset = rectangle.top
val inflater =
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())
val rootView = inflater.inflate(R.layout.dev_loading_view, null) as ViewGroup
val textView = rootView.findViewById<TextView>(R.id.loading_text)
textView.text = message

val dismissButtonView = rootView.findViewById<android.widget.Button>(R.id.dismiss_button)

if (dismissButton) {
dismissButtonView.visibility = android.view.View.VISIBLE
} else {
dismissButtonView.visibility = android.view.View.GONE
}
if (backgroundColor != null) {
view.setBackgroundColor(backgroundColor.toInt())

// Use provided colors or defaults (matching iOS behavior)
val textColor = color?.toInt() ?: Color.WHITE
val bgColor = backgroundColor?.toInt() ?: Color.rgb(64, 64, 64) // Default grey

textView.setTextColor(textColor)
rootView.setBackgroundColor(bgColor)

if (dismissButton) {
dismissButtonView.setTextColor(textColor)

// Darken the background color for the button
val red = (Color.red(bgColor) * 0.7).toInt()
val green = (Color.green(bgColor) * 0.7).toInt()
val blue = (Color.blue(bgColor) * 0.7).toInt()
val darkerColor = Color.rgb(red, green, blue)

// Create rounded drawable for button
val drawable = android.graphics.drawable.GradientDrawable()
drawable.setColor(darkerColor)
drawable.cornerRadius = 15 * rootView.resources.displayMetrics.density
dismissButtonView.background = drawable

dismissButtonView.setOnClickListener { hideInternal() }
}
view.setOnClickListener { hideInternal() }

// Allow tapping anywhere on the banner to dismiss
rootView.setOnClickListener { hideInternal() }

val popup =
PopupWindow(
view,
rootView,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
devLoadingView = view
devLoadingView = textView // Store the TextView for updateProgress()
devLoadingPopup = popup

// TODO T164786028: Find out the root cause of the BadTokenException exception here
} catch (e: WindowManager.BadTokenException) {
FLog.e(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ 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 showMessage(
message: String,
color: Double?,
backgroundColor: Double?,
dismissButton: Boolean?,
)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ internal class DevLoadingModule(reactContext: ReactApplicationContext) :
}
}

override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
override fun showMessage(
message: String,
color: Double?,
backgroundColor: Double?,
dismissButton: Boolean?,
) {
UiThreadUtil.runOnUiThread {
devLoadingViewManager?.showMessage(message, color, backgroundColor)
devLoadingViewManager?.showMessage(message, color, backgroundColor, dismissButton)
}
}

Expand Down
Loading