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