react-native-fast-image 8.6.43 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -213,7 +213,7 @@ If supplied, changes the color of all the non-transparent pixels to the given co
213
213
 
214
214
  ## Static Methods
215
215
 
216
- ### `FastImage.preload: (source[]) => void`
216
+ ### `FastImage.preload: (source[]) => Promise<result[]>`
217
217
 
218
218
  Preload images to display later. e.g.
219
219
 
@@ -230,6 +230,13 @@ FastImage.preload([
230
230
  ])
231
231
  ```
232
232
 
233
+ It resolves when all the images have loaded or failed, with a result for each source, in order: `{ uri, ok, width, height, error }`. `width` and `height` are the image's size (as in `onLoad`), which is also a way to find an image's size without showing it. A few sources load at a time (3, or `SDWebImagePrefetcher`'s `maxConcurrentPrefetchCount` on iOS), so a long list doesn't hold up the images the app is showing, and at low priority unless the source sets `priority`. It never rejects:
234
+
235
+ ```js
236
+ const results = await FastImage.preload(sources)
237
+ const failed = results.filter((result) => !result.ok)
238
+ ```
239
+
233
240
  ### `FastImage.clearMemoryCache: () => Promise<void>`
234
241
 
235
242
  Clear all images from memory cache.
@@ -1,24 +1,34 @@
1
1
  package com.dylanvann.fastimage;
2
2
 
3
3
  import android.app.Activity;
4
- import android.util.Log;
4
+ import android.graphics.drawable.Drawable;
5
5
 
6
6
  import androidx.annotation.NonNull;
7
+ import androidx.annotation.Nullable;
7
8
 
8
9
  import com.bumptech.glide.Glide;
9
- import com.bumptech.glide.load.model.GlideUrl;
10
+ import com.bumptech.glide.Priority;
11
+ import com.bumptech.glide.load.DataSource;
12
+ import com.bumptech.glide.load.engine.GlideException;
13
+ import com.bumptech.glide.request.RequestListener;
14
+ import com.bumptech.glide.request.RequestOptions;
15
+ import com.bumptech.glide.request.target.Target;
16
+ import com.facebook.react.bridge.Arguments;
10
17
  import com.facebook.react.bridge.Promise;
11
18
  import com.facebook.react.bridge.ReactApplicationContext;
12
19
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
13
20
  import com.facebook.react.bridge.ReactMethod;
14
21
  import com.facebook.react.bridge.ReadableArray;
15
22
  import com.facebook.react.bridge.ReadableMap;
16
- import com.facebook.react.views.imagehelper.ImageSource;
23
+ import com.facebook.react.bridge.UiThreadUtil;
24
+ import com.facebook.react.bridge.WritableArray;
25
+ import com.facebook.react.bridge.WritableMap;
26
+
27
+ import java.util.ArrayDeque;
17
28
 
18
29
  class FastImageViewModule extends ReactContextBaseJavaModule {
19
30
 
20
31
  private static final String REACT_CLASS = "FastImageView";
21
- private static final String TAG = "FastImage";
22
32
 
23
33
  FastImageViewModule(ReactApplicationContext reactContext) {
24
34
  super(reactContext);
@@ -30,41 +40,141 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
30
40
  return REACT_CLASS;
31
41
  }
32
42
 
43
+ // At most this many preloaded sources load at a time, across all preload
44
+ // calls (the same as SDWebImagePrefetcher's default on iOS), so a long
45
+ // list doesn't queue hundreds of requests ahead of the images the app is
46
+ // showing (Glide's executors run requests in order within a priority).
47
+ private static final int PRELOAD_LIMIT = 3;
48
+ // Preloads waiting to start, in the order they were added, and the number
49
+ // loading. Only used on the UI thread (Glide calls its listeners there).
50
+ private static final ArrayDeque<Runnable> pendingPreloads = new ArrayDeque<>();
51
+ private static int preloadsInFlight = 0;
52
+
53
+ // Starts pending preloads while there's room. A listener calls it again,
54
+ // sometimes from inside run() (Glide reports a memory-cached image
55
+ // synchronously from preload()). That's fine: the counters are shared and
56
+ // updated before each run(), so the inner call starts what fits, and the
57
+ // outer loop sees the updated counters when it checks again.
58
+ private static void startPendingPreloads() {
59
+ while (preloadsInFlight < PRELOAD_LIMIT && !pendingPreloads.isEmpty()) {
60
+ preloadsInFlight++;
61
+ pendingPreloads.poll().run();
62
+ }
63
+ }
64
+
65
+ // Resolves with a result per source, in order, once all have loaded or
66
+ // failed: { ok, width, height } or { ok: false, error }. Never rejects.
33
67
  @ReactMethod
34
- public void preload(final ReadableArray sources) {
35
- final Activity activity = getCurrentActivity();
36
- if (activity == null) return;
37
- activity.runOnUiThread(new Runnable() {
68
+ public void preload(final ReadableArray sources, final Promise promise) {
69
+ final ReactApplicationContext context = getReactApplicationContext();
70
+ UiThreadUtil.runOnUiThread(new Runnable() {
38
71
  @Override
39
72
  public void run() {
40
- for (int i = 0; i < sources.size(); i++) {
41
- final ReadableMap source = sources.getMap(i);
42
- // Skip sources without a uri (Glide throws on an empty url).
43
- // preload has no way to report errors, so log them.
73
+ final int count = sources.size();
74
+ final WritableMap[] results = new WritableMap[count];
75
+ // Sources of this call still to finish. It starts at the
76
+ // number of sources, so the promise can't resolve before
77
+ // they've all been added.
78
+ final int[] remaining = {count};
79
+ final Runnable finishOne = new Runnable() {
80
+ @Override
81
+ public void run() {
82
+ if (--remaining[0] > 0) return;
83
+ WritableArray array = Arguments.createArray();
84
+ for (WritableMap result : results) array.pushMap(result);
85
+ promise.resolve(array);
86
+ }
87
+ };
88
+ for (int i = 0; i < count; i++) {
89
+ final int index = i;
90
+ final ReadableMap source = sources.isNull(i) ? null : sources.getMap(i);
91
+ // Glide throws on an empty url. Invalid sources fail
92
+ // without taking a slot.
44
93
  if (!FastImageViewConverter.hasUri(source)) {
45
- Log.w(TAG, "preload: skipping a source without a uri");
94
+ results[i] = failure("Invalid source: no uri");
95
+ finishOne.run();
46
96
  continue;
47
97
  }
48
- final FastImageSource imageSource = FastImageViewConverter.getImageSource(activity, source);
49
- // Also skip a uri that can't be resolved (e.g. a relative path),
50
- // which resolves to an empty one. The view reports it as an error.
98
+ final FastImageSource imageSource = FastImageViewConverter.getImageSource(context, source);
99
+ // A uri that can't be resolved (e.g. a relative path) resolves
100
+ // to an empty one.
51
101
  if (imageSource.getUri().toString().isEmpty()) {
52
- Log.w(TAG, "preload: skipping a uri that can't be resolved: " + source.getString("uri"));
102
+ results[i] = failure("Invalid source: can't resolve " + source.getString("uri"));
103
+ finishOne.run();
53
104
  continue;
54
105
  }
106
+ RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
107
+ // Low priority unless the source sets one, as on iOS (the
108
+ // prefetcher's options), so the images the app shows load
109
+ // first.
110
+ final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
111
+ ? options
112
+ : options.priority(Priority.LOW);
113
+ pendingPreloads.add(new Runnable() {
114
+ @Override
115
+ public void run() {
116
+ Glide
117
+ .with(context)
118
+ // Load it the way the view does, so local images
119
+ // (file://, content://, asset:/) work too.
120
+ .load(imageSource.getSourceForLoad())
121
+ .apply(preloadOptions)
122
+ .listener(new RequestListener<Drawable>() {
123
+ @Override
124
+ public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
125
+ results[index] = failure(errorMessage(e));
126
+ preloadsInFlight--;
127
+ finishOne.run();
128
+ startPendingPreloads();
129
+ return false;
130
+ }
55
131
 
56
- Glide
57
- .with(activity.getApplicationContext())
58
- // Load it the way the view does, so local images
59
- // (file://, content://, asset:/) work too.
60
- .load(imageSource.getSourceForLoad())
61
- .apply(FastImageViewConverter.getOptions(activity, imageSource, source))
62
- .preload();
132
+ @Override
133
+ public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
134
+ // Preloaded at its original size, so this is
135
+ // the image's own size.
136
+ WritableMap result = Arguments.createMap();
137
+ result.putBoolean("ok", true);
138
+ result.putInt("width", resource.getIntrinsicWidth());
139
+ result.putInt("height", resource.getIntrinsicHeight());
140
+ results[index] = result;
141
+ preloadsInFlight--;
142
+ finishOne.run();
143
+ startPendingPreloads();
144
+ return false;
145
+ }
146
+ })
147
+ .preload();
148
+ }
149
+ });
63
150
  }
151
+ if (count == 0) {
152
+ promise.resolve(Arguments.createArray());
153
+ return;
154
+ }
155
+ startPendingPreloads();
64
156
  }
65
157
  });
66
158
  }
67
159
 
160
+ private static WritableMap failure(String error) {
161
+ WritableMap result = Arguments.createMap();
162
+ result.putBoolean("ok", false);
163
+ result.putString("error", error);
164
+ return result;
165
+ }
166
+
167
+ // The first root cause's message, e.g. "Not Found, status code: 404".
168
+ private static String errorMessage(@Nullable GlideException e) {
169
+ if (e != null) {
170
+ for (Throwable cause : e.getRootCauses()) {
171
+ if (cause.getMessage() != null) return cause.getMessage();
172
+ }
173
+ if (e.getMessage() != null) return e.getMessage();
174
+ }
175
+ return "Failed to load the image";
176
+ }
177
+
68
178
  @ReactMethod
69
179
  public void clearMemoryCache(final Promise promise) {
70
180
  final Activity activity = getCurrentActivity();
package/dist/index.cjs.js CHANGED
@@ -129,7 +129,16 @@ const FastImage = FastImageComponent;
129
129
  FastImage.resizeMode = resizeMode;
130
130
  FastImage.cacheControl = cacheControl;
131
131
  FastImage.priority = priority;
132
- FastImage.preload = (sources) => react_native.NativeModules.FastImageView.preload(sources);
132
+ FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
133
+ var _results$i;
134
+ return {
135
+ uri: source ? source.uri : void 0,
136
+ ...(_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : {
137
+ ok: false,
138
+ error: "No result"
139
+ }
140
+ };
141
+ }));
133
142
  FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
134
143
  FastImage.clearDiskCache = () => react_native.NativeModules.FastImageView.clearDiskCache();
135
144
  const styles = react_native.StyleSheet.create({
@@ -43,7 +43,17 @@ export type ResizeModes = $Values<ResizeMode>
43
43
  export type Priorities = $Values<Priority>
44
44
  export type CacheControls = $Values<CacheControl>
45
45
 
46
- export type PreloadFn = (sources: Array<FastImageSource>) => void
46
+ export type PreloadResult = {|
47
+ uri?: string,
48
+ ok: boolean,
49
+ width?: number,
50
+ height?: number,
51
+ error?: string,
52
+ |}
53
+
54
+ export type PreloadFn = (
55
+ sources: Array<FastImageSource>,
56
+ ) => Promise<Array<PreloadResult>>
47
57
  export type FastImageSource = {
48
58
  uri?: string,
49
59
  headers?: Object,
package/dist/index.d.ts CHANGED
@@ -93,11 +93,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
93
93
  */
94
94
  children?: React.ReactNode;
95
95
  }
96
+ export interface PreloadResult {
97
+ uri?: string;
98
+ ok: boolean;
99
+ width?: number;
100
+ height?: number;
101
+ error?: string;
102
+ }
96
103
  export interface FastImageStaticProperties {
97
104
  resizeMode: typeof resizeMode;
98
105
  priority: typeof priority;
99
106
  cacheControl: typeof cacheControl;
100
- preload: (sources: Source[]) => void;
107
+ preload: (sources: Source[]) => Promise<PreloadResult[]>;
101
108
  clearMemoryCache: () => Promise<void>;
102
109
  clearDiskCache: () => Promise<void>;
103
110
  }
package/dist/index.js CHANGED
@@ -106,7 +106,16 @@ const FastImage = FastImageComponent;
106
106
  FastImage.resizeMode = resizeMode;
107
107
  FastImage.cacheControl = cacheControl;
108
108
  FastImage.priority = priority;
109
- FastImage.preload = (sources) => NativeModules.FastImageView.preload(sources);
109
+ FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.preload(sources.map((s) => s || {}))).then((results) => sources.map((source, i) => {
110
+ var _results$i;
111
+ return {
112
+ uri: source ? source.uri : void 0,
113
+ ...(_results$i = results === null || results === void 0 ? void 0 : results[i]) !== null && _results$i !== void 0 ? _results$i : {
114
+ ok: false,
115
+ error: "No result"
116
+ }
117
+ };
118
+ }));
110
119
  FastImage.clearMemoryCache = () => NativeModules.FastImageView.clearMemoryCache();
111
120
  FastImage.clearDiskCache = () => NativeModules.FastImageView.clearDiskCache();
112
121
  const styles = StyleSheet.create({
@@ -43,7 +43,17 @@ export type ResizeModes = $Values<ResizeMode>
43
43
  export type Priorities = $Values<Priority>
44
44
  export type CacheControls = $Values<CacheControl>
45
45
 
46
- export type PreloadFn = (sources: Array<FastImageSource>) => void
46
+ export type PreloadResult = {|
47
+ uri?: string,
48
+ ok: boolean,
49
+ width?: number,
50
+ height?: number,
51
+ error?: string,
52
+ |}
53
+
54
+ export type PreloadFn = (
55
+ sources: Array<FastImageSource>,
56
+ ) => Promise<Array<PreloadResult>>
47
57
  export type FastImageSource = {
48
58
  uri?: string,
49
59
  headers?: Object,
@@ -21,6 +21,8 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
21
21
  @property (nonatomic) NSURL* url;
22
22
  // priority for image request
23
23
  @property (nonatomic) FFFPriority priority;
24
+ // whether the source set a priority (preload is low priority otherwise)
25
+ @property (nonatomic) BOOL hasPriority;
24
26
  // headers for the image request
25
27
  @property (nonatomic) NSDictionary *headers;
26
28
  // cache control mode
@@ -1,9 +1,9 @@
1
1
  #import "FFFastImageViewManager.h"
2
2
  #import "FFFastImageView.h"
3
- #import <React/RCTLog.h>
4
3
 
5
4
  #import <SDWebImage/SDImageCache.h>
6
5
  #import <SDWebImage/SDWebImageManager.h>
6
+ #import <SDWebImage/SDWebImageError.h>
7
7
  #import <SDWebImage/SDWebImagePrefetcher.h>
8
8
 
9
9
  @implementation FFFastImageViewManager
@@ -24,36 +24,119 @@ RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
24
24
  RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
25
25
  RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
26
26
 
27
- RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources)
27
+ // The error's description, with the HTTP status code when there is one.
28
+ static NSString *FFFErrorMessage(NSError *error)
28
29
  {
29
- SDWebImagePrefetcher *prefetcher = [SDWebImagePrefetcher sharedImagePrefetcher];
30
- NSMutableArray *urls = [NSMutableArray arrayWithCapacity:sources.count];
30
+ NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
31
+ if (statusCode) {
32
+ return [NSString stringWithFormat:@"%@, status code: %@", error.localizedDescription, statusCode];
33
+ }
34
+ return error.localizedDescription ?: @"Failed to load the image";
35
+ }
31
36
 
32
- [sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
33
- // Skip sources without a url (an empty, missing or null uri). NSArray can't
34
- // hold nil, so adding one throws and crashes the app.
35
- if (!source.url) {
36
- // preload has no way to report errors, so log it.
37
- RCTLogWarn(@"FastImage.preload: skipping a source without a valid uri");
38
- return;
37
+ // Preloads waiting to start, in the order they were added, and the number
38
+ // loading, across all preload calls. Like SDWebImagePrefetcher, at most
39
+ // maxConcurrentPrefetchCount load at a time, so a long list doesn't queue
40
+ // hundreds of operations ahead of the images the app is showing. Only used on
41
+ // the main queue (SDWebImageManager calls its completion blocks there).
42
+ static NSMutableArray<dispatch_block_t> *FFFPendingPreloads;
43
+ static NSUInteger FFFPreloadsInFlight;
44
+
45
+ static NSUInteger FFFPreloadLimit(void)
46
+ {
47
+ NSUInteger limit = [SDWebImagePrefetcher sharedImagePrefetcher].maxConcurrentPrefetchCount;
48
+ return MAX(limit, (NSUInteger)1);
49
+ }
50
+
51
+ // Starts pending preloads while there's room. A completion block calls it
52
+ // again, sometimes from inside start() (a memory cache hit completes
53
+ // synchronously on the main queue). That's fine: the counters are shared and
54
+ // updated before each start(), so the inner call starts what fits, and the
55
+ // outer loop sees the updated counters when it checks again.
56
+ static void FFFStartPendingPreloads(void)
57
+ {
58
+ while (FFFPreloadsInFlight < FFFPreloadLimit() && FFFPendingPreloads.count > 0) {
59
+ dispatch_block_t start = FFFPendingPreloads.firstObject;
60
+ [FFFPendingPreloads removeObjectAtIndex:0];
61
+ FFFPreloadsInFlight++;
62
+ start();
63
+ }
64
+ }
65
+
66
+ // Resolves with a result per source, in order, once all have loaded or failed:
67
+ // { ok, width, height } or { ok: false, error }. Never rejects.
68
+ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
69
+ resolve:(RCTPromiseResolveBlock)resolve
70
+ reject:(__unused RCTPromiseRejectBlock)reject)
71
+ {
72
+ // This runs on the UIManager queue; the queue state is only touched on
73
+ // the main queue.
74
+ dispatch_async(dispatch_get_main_queue(), ^{
75
+ if (!FFFPendingPreloads) {
76
+ FFFPendingPreloads = [NSMutableArray array];
39
77
  }
40
- if (source.headers.count == 0) {
41
- [urls addObject:source.url];
78
+ NSMutableArray *results = [NSMutableArray arrayWithCapacity:sources.count];
79
+ // Sources of this call still to finish. It starts at the number of
80
+ // sources, so the promise can't resolve before they've all been added.
81
+ __block NSUInteger remaining = sources.count;
82
+ void (^finishOne)(void) = ^{
83
+ if (--remaining == 0) {
84
+ resolve(results);
85
+ }
86
+ };
87
+ // With the prefetcher's options (low priority), but one source at a
88
+ // time (not SDWebImagePrefetcher), to get each source's result and to
89
+ // send its headers with its own request only.
90
+ SDWebImageOptions prefetcherOptions = [SDWebImagePrefetcher sharedImagePrefetcher].options;
91
+
92
+ [sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
93
+ if (!source.url) {
94
+ // An empty, missing or null uri (JS sends null sources as {}).
95
+ // It fails without taking a slot.
96
+ [results addObject:@{@"ok": @NO, @"error": @"Invalid source: no uri"}];
97
+ finishOne();
98
+ return;
99
+ }
100
+ [results addObject:[NSNull null]];
101
+ // A source's own priority replaces the prefetcher's, as on Android.
102
+ SDWebImageOptions options = prefetcherOptions;
103
+ if (source.hasPriority) {
104
+ options &= ~(SDWebImageLowPriority | SDWebImageHighPriority);
105
+ if (source.priority == FFFPriorityLow) {
106
+ options |= SDWebImageLowPriority;
107
+ } else if (source.priority == FFFPriorityHigh) {
108
+ options |= SDWebImageHighPriority;
109
+ }
110
+ }
111
+ [FFFPendingPreloads addObject:[^{
112
+ // Once per slot: SDWebImage calls this once with finished set
113
+ // (a progressive load calls it more, without).
114
+ __block BOOL done = NO;
115
+ [[SDWebImageManager sharedManager] loadImageWithURL:source.url
116
+ options:options
117
+ context:@{SDWebImageContextDownloadRequestModifier: source.requestModifier}
118
+ progress:nil
119
+ completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
120
+ if (!finished || done) {
121
+ return;
122
+ }
123
+ done = YES;
124
+ results[idx] = image
125
+ ? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
126
+ : @{@"ok": @NO, @"error": FFFErrorMessage(error)};
127
+ FFFPreloadsInFlight--;
128
+ finishOne();
129
+ FFFStartPendingPreloads();
130
+ }];
131
+ } copy]];
132
+ }];
133
+
134
+ if (sources.count == 0) {
135
+ resolve(results);
42
136
  return;
43
137
  }
44
- // Send the headers with this source's request only (setting them on
45
- // the shared downloader sent them with every later request). The
46
- // prefetcher can't take headers per request in SDWebImage 5.11 (its
47
- // context is shared by all prefetches), so load it the way the
48
- // prefetcher does, with the same options.
49
- [[SDWebImageManager sharedManager] loadImageWithURL:source.url
50
- options:prefetcher.options
51
- context:@{SDWebImageContextDownloadRequestModifier: source.requestModifier}
52
- progress:nil
53
- completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {}];
54
- }];
55
-
56
- [prefetcher prefetchURLs:urls];
138
+ FFFStartPendingPreloads();
139
+ });
57
140
  }
58
141
 
59
142
  RCT_EXPORT_METHOD(clearMemoryCache:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
@@ -45,6 +45,8 @@ RCT_ENUM_CONVERTER(FFFCacheControl, (@{
45
45
  }
46
46
 
47
47
  FFFastImageSource *imageSource = [[FFFastImageSource alloc] initWithURL:uri priority:priority headers:headers cacheControl:cacheControl];
48
+ id priorityJSON = json[@"priority"];
49
+ imageSource.hasPriority = priorityJSON && priorityJSON != (id)kCFNull;
48
50
 
49
51
  return imageSource;
50
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.6.43",
3
+ "version": "8.7.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",