1/* 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import <ABI47_0_0React/ABI47_0_0RCTDevLoadingView.h> 9 10#import <QuartzCore/QuartzCore.h> 11 12#import <ABI47_0_0FBReactNativeSpec/ABI47_0_0FBReactNativeSpec.h> 13#import <ABI47_0_0React/ABI47_0_0RCTAppearance.h> 14#import <ABI47_0_0React/ABI47_0_0RCTBridge.h> 15#import <ABI47_0_0React/ABI47_0_0RCTConvert.h> 16#import <ABI47_0_0React/ABI47_0_0RCTDefines.h> 17#import <ABI47_0_0React/ABI47_0_0RCTDevLoadingViewSetEnabled.h> 18#import <ABI47_0_0React/ABI47_0_0RCTUtils.h> 19 20#import "ABI47_0_0CoreModulesPlugins.h" 21 22using namespace ABI47_0_0facebook::ABI47_0_0React; 23 24@interface ABI47_0_0RCTDevLoadingView () <ABI47_0_0NativeDevLoadingViewSpec> 25@end 26 27#if ABI47_0_0RCT_DEV | ABI47_0_0RCT_ENABLE_LOADING_VIEW 28 29@implementation ABI47_0_0RCTDevLoadingView { 30 UIWindow *_window; 31 UILabel *_label; 32 NSDate *_showDate; 33 BOOL _hiding; 34 dispatch_block_t _initialMessageBlock; 35} 36 37@synthesize bundleManager = _bundleManager; 38 39ABI47_0_0RCT_EXPORT_MODULE() 40 41- (instancetype)init 42{ 43 if (self = [super init]) { 44 [[NSNotificationCenter defaultCenter] addObserver:self 45 selector:@selector(hide) 46 name:ABI47_0_0RCTJavaScriptDidLoadNotification 47 object:nil]; 48 [[NSNotificationCenter defaultCenter] addObserver:self 49 selector:@selector(hide) 50 name:ABI47_0_0RCTJavaScriptDidFailToLoadNotification 51 object:nil]; 52 } 53 return self; 54} 55 56+ (void)setEnabled:(BOOL)enabled 57{ 58 ABI47_0_0RCTDevLoadingViewSetEnabled(enabled); 59} 60 61+ (BOOL)requiresMainQueueSetup 62{ 63 return NO; 64} 65 66- (void)clearInitialMessageDelay 67{ 68 if (self->_initialMessageBlock != nil) { 69 dispatch_block_cancel(self->_initialMessageBlock); 70 self->_initialMessageBlock = nil; 71 } 72} 73 74- (void)showInitialMessageDelayed:(void (^)())initialMessage 75{ 76 self->_initialMessageBlock = dispatch_block_create(static_cast<dispatch_block_flags_t>(0), initialMessage); 77 78 // We delay the initial loading message to prevent flashing it 79 // when loading progress starts quickly. To do that, we 80 // schedule the message to be shown in a block, and cancel 81 // the block later when the progress starts coming in. 82 // If the progress beats this timer, this message is not shown. 83 dispatch_after( 84 dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock); 85} 86 87- (UIColor *)dimColor:(UIColor *)c 88{ 89 // Given a color, return a slightly lighter or darker color for dim effect. 90 CGFloat h, s, b, a; 91 if ([c getHue:&h saturation:&s brightness:&b alpha:&a]) 92 return [UIColor colorWithHue:h saturation:s brightness:b < 0.5 ? b * 1.25 : b * 0.75 alpha:a]; 93 return nil; 94} 95 96- (NSString *)getTextForHost 97{ 98 NSURL *bundleURL = _bundleManager.bundleURL; 99 if (bundleURL == nil || bundleURL.fileURL) { 100 return @"ABI47_0_0React Native"; 101 } 102 103 return [NSString stringWithFormat:@"%@:%@", bundleURL.host, bundleURL.port]; 104} 105 106- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor 107{ 108 if (!ABI47_0_0RCTDevLoadingViewGetEnabled() || self->_hiding) { 109 return; 110 } 111 112 dispatch_async(dispatch_get_main_queue(), ^{ 113 self->_showDate = [NSDate date]; 114 if (!self->_window && !ABI47_0_0RCTRunningInTestEnvironment()) { 115 CGSize screenSize = [UIScreen mainScreen].bounds.size; 116 117 UIWindow *window = ABI47_0_0RCTSharedApplication().keyWindow; 118 self->_window = 119 [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, window.safeAreaInsets.top + 10)]; 120 self->_label = 121 [[UILabel alloc] initWithFrame:CGRectMake(0, window.safeAreaInsets.top - 10, screenSize.width, 20)]; 122 [self->_window addSubview:self->_label]; 123 124 self->_window.windowLevel = UIWindowLevelStatusBar + 1; 125 // set a root VC so rotation is supported 126 self->_window.rootViewController = [UIViewController new]; 127 128 self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular]; 129 self->_label.textAlignment = NSTextAlignmentCenter; 130 } 131 132 self->_label.text = message; 133 self->_label.textColor = color; 134 135 self->_window.backgroundColor = backgroundColor; 136 self->_window.hidden = NO; 137 138#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ 139 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 140 if (@available(iOS 13.0, *)) { 141 UIWindowScene *scene = (UIWindowScene *)ABI47_0_0RCTSharedApplication().connectedScenes.anyObject; 142 self->_window.windowScene = scene; 143 } 144#endif 145 }); 146} 147 148ABI47_0_0RCT_EXPORT_METHOD(showMessage 149 : (NSString *)message withColor 150 : (NSNumber *__nonnull)color withBackgroundColor 151 : (NSNumber *__nonnull)backgroundColor) 152{ 153 [self showMessage:message color:[ABI47_0_0RCTConvert UIColor:color] backgroundColor:[ABI47_0_0RCTConvert UIColor:backgroundColor]]; 154} 155 156ABI47_0_0RCT_EXPORT_METHOD(hide) 157{ 158 if (!ABI47_0_0RCTDevLoadingViewGetEnabled()) { 159 return; 160 } 161 162 // Cancel the initial message block so it doesn't display later and get stuck. 163 [self clearInitialMessageDelay]; 164 165 dispatch_async(dispatch_get_main_queue(), ^{ 166 self->_hiding = true; 167 const NSTimeInterval MIN_PRESENTED_TIME = 0.6; 168 NSTimeInterval presentedTime = [[NSDate date] timeIntervalSinceDate:self->_showDate]; 169 NSTimeInterval delay = MAX(0, MIN_PRESENTED_TIME - presentedTime); 170 CGRect windowFrame = self->_window.frame; 171 [UIView animateWithDuration:0.25 172 delay:delay 173 options:0 174 animations:^{ 175 self->_window.frame = CGRectOffset(windowFrame, 0, -windowFrame.size.height); 176 } 177 completion:^(__unused BOOL finished) { 178 self->_window.frame = windowFrame; 179 self->_window.hidden = YES; 180 self->_window = nil; 181 self->_hiding = false; 182 }]; 183 }); 184} 185 186- (void)showProgressMessage:(NSString *)message 187{ 188 if (self->_window != nil) { 189 // This is an optimization. Since the progress can come in quickly, 190 // we want to do the minimum amount of work to update the UI, 191 // which is to only update the label text. 192 self->_label.text = message; 193 return; 194 } 195 196 UIColor *color = [UIColor whiteColor]; 197 UIColor *backgroundColor = [UIColor colorWithHue:105 saturation:0 brightness:.25 alpha:1]; 198 199 if ([self isDarkModeEnabled]) { 200 color = [UIColor colorWithHue:208 saturation:0.03 brightness:.14 alpha:1]; 201 backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1]; 202 } 203 204 [self showMessage:message color:color backgroundColor:backgroundColor]; 205} 206 207- (void)showOfflineMessage 208{ 209 UIColor *color = [UIColor whiteColor]; 210 UIColor *backgroundColor = [UIColor blackColor]; 211 212 if ([self isDarkModeEnabled]) { 213 color = [UIColor blackColor]; 214 backgroundColor = [UIColor whiteColor]; 215 } 216 217 NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", ABI47_0_0RCT_PACKAGER_NAME]; 218 [self showMessage:message color:color backgroundColor:backgroundColor]; 219} 220 221- (BOOL)isDarkModeEnabled 222{ 223 // We pass nil here to match the behavior of the native module. 224 // If we were to pass a view, then it's possible that this native 225 // banner would have a different color than the JavaScript banner 226 // (which always passes nil). This would result in an inconsistent UI. 227 return [ABI47_0_0RCTColorSchemePreference(nil) isEqualToString:@"dark"]; 228} 229- (void)showWithURL:(NSURL *)URL 230{ 231 if (URL.fileURL) { 232 // If dev mode is not enabled, we don't want to show this kind of notification. 233#if !ABI47_0_0RCT_DEV 234 return; 235#endif 236 [self showOfflineMessage]; 237 } else { 238 [self showInitialMessageDelayed:^{ 239 NSString *message = [NSString stringWithFormat:@"Loading from %@\u2026", ABI47_0_0RCT_PACKAGER_NAME]; 240 [self showProgressMessage:message]; 241 }]; 242 } 243} 244 245- (void)updateProgress:(ABI47_0_0RCTLoadingProgress *)progress 246{ 247 if (!progress) { 248 return; 249 } 250 251 // Cancel the initial message block so it's not flashed before progress. 252 [self clearInitialMessageDelay]; 253 254 dispatch_async(dispatch_get_main_queue(), ^{ 255 [self showProgressMessage:[progress description]]; 256 }); 257} 258 259- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params 260{ 261 return std::make_shared<NativeDevLoadingViewSpecJSI>(params); 262} 263 264@end 265 266#else 267 268@implementation ABI47_0_0RCTDevLoadingView 269 270+ (NSString *)moduleName 271{ 272 return nil; 273} 274+ (void)setEnabled:(BOOL)enabled 275{ 276} 277- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor 278{ 279} 280- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor 281{ 282} 283- (void)showWithURL:(NSURL *)URL 284{ 285} 286- (void)updateProgress:(ABI47_0_0RCTLoadingProgress *)progress 287{ 288} 289- (void)hide 290{ 291} 292- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params 293{ 294 return std::make_shared<NativeDevLoadingViewSpecJSI>(params); 295} 296 297@end 298 299#endif 300 301Class ABI47_0_0RCTDevLoadingViewCls(void) 302{ 303 return ABI47_0_0RCTDevLoadingView.class; 304} 305