expo-image 1.2.0 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.2.2 — 2023-04-27
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fix for the "limited" media library permission. ([#22261](https://github.com/expo/expo/pull/22261) by [@tsapeta](https://github.com/tsapeta))
18
+
19
+ ## 1.2.1 — 2023-04-17
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - [Android] Fix `url` property returned by the `onLoad` event. ([#22161](https://github.com/expo/expo/pull/22161) by [@lukmccall](https://github.com/lukmccall))
24
+ - [Android] Fix images not loading after the app was foregrounded. ([#22159](https://github.com/expo/expo/pull/22159) by [@lukmccall](https://github.com/lukmccall))
25
+ - [Android] Fixed image was loaded event if the view dimensions were 0. ([#22157](https://github.com/expo/expo/pull/22157) by [@lukmccall](https://github.com/lukmccall))
26
+ - Fix generating the image from ThumbHash that starts with a slash character. ([#22160](https://github.com/expo/expo/pull/22160) by [@tsapeta](https://github.com/tsapeta))
27
+
13
28
  ## 1.2.0 — 2023-04-14
14
29
 
15
30
  ### 🎉 New features
@@ -21,7 +36,9 @@
21
36
 
22
37
  ### 🐛 Bug fixes
23
38
 
39
+ - [Web] Improve transition behavior when switching back and forth two sources. ([#22099](https://github.com/expo/expo/pull/22099) by [@aleqsio](https://github.com/aleqsio))
24
40
  - [Web] Prevent breaking in static rendering environments. ([#21883](https://github.com/expo/expo/pull/21883) by [@EvanBacon](https://github.com/EvanBacon))
41
+ - [Android] Fixed image disappearing before navigation animation is complete. ([#22066](https://github.com/expo/expo/pull/22066) by [@sallen450](https://github.com/sallen450))
25
42
  - [Web] Fixed monorepo asset resolution in production for Metro web. ([#22094](https://github.com/expo/expo/pull/22094) by [@EvanBacon](https://github.com/EvanBacon))
26
43
 
27
44
  ## 1.1.0 — 2023-03-25
package/README.md CHANGED
@@ -21,17 +21,17 @@ A cross-platform, performant image component for React Native and Expo.
21
21
 
22
22
  ## Supported image formats
23
23
 
24
- | Format | Android | iOS | Web |
25
- |:---:|:---:|:---:|:---:|
26
- | WebP | | ✅ | ✅ [~96% adoption](https://caniuse.com/webp) |
27
- | PNG / APNG | | ✅ | ✅ / ✅ [~96% adoption](https://caniuse.com/apng) |
28
- | AVIF | | ✅ | ⏳ [~79% adoption](https://caniuse.com/avif) |
29
- | HEIC | | ✅ | ❌ [not adopted yet](https://caniuse.com/heif) |
30
- | JPEG | | ✅ | |
31
- | GIF | | ✅ | |
32
- | SVG | | ✅ | |
33
- | ICO | | ✅ | |
34
- | ICNS | | ✅ | |
24
+ | Format | Android | iOS | Web |
25
+ | :--------: | :-----: | :-: | :-----------------------------------------------: |
26
+ | WebP | | ✅ | ✅ [~96% adoption](https://caniuse.com/webp) |
27
+ | PNG / APNG | | ✅ | ✅ / ✅ [~96% adoption](https://caniuse.com/apng) |
28
+ | AVIF | | ✅ | ⏳ [~79% adoption](https://caniuse.com/avif) |
29
+ | HEIC | | ✅ | ❌ [not adopted yet](https://caniuse.com/heif) |
30
+ | JPEG | | ✅ | |
31
+ | GIF | | ✅ | |
32
+ | SVG | | ✅ | |
33
+ | ICO | | ✅ | |
34
+ | ICNS | | ✅ | |
35
35
 
36
36
  # API documentation
37
37
 
@@ -49,7 +49,7 @@ android {
49
49
  minSdkVersion safeExtGet("minSdkVersion", 21)
50
50
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
51
51
  versionCode 1
52
- versionName "1.2.0"
52
+ versionName "1.2.2"
53
53
  }
54
54
  lintOptions {
55
55
  abortOnError false
@@ -1,6 +1,7 @@
1
1
  package expo.modules.image
2
2
 
3
3
  import android.view.View
4
+ import androidx.core.view.doOnDetach
4
5
  import com.bumptech.glide.Glide
5
6
  import com.bumptech.glide.load.model.GlideUrl
6
7
  import com.facebook.react.uimanager.PixelUtil
@@ -171,7 +172,9 @@ class ExpoImageModule : Module() {
171
172
  }
172
173
 
173
174
  OnViewDestroys { view: ExpoImageViewWrapper ->
174
- view.onViewDestroys()
175
+ view.doOnDetach {
176
+ view.onViewDestroys()
177
+ }
175
178
  }
176
179
  }
177
180
  }
@@ -3,7 +3,6 @@ package expo.modules.image
3
3
  import android.annotation.SuppressLint
4
4
  import android.app.Activity
5
5
  import android.content.Context
6
- import android.graphics.Rect
7
6
  import android.graphics.drawable.Animatable
8
7
  import android.graphics.drawable.Drawable
9
8
  import android.os.Build
@@ -371,13 +370,11 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
371
370
  return sources.first()
372
371
  }
373
372
 
374
- val selfRect = Rect(0, 0, width, height)
375
- if (selfRect.isEmpty) {
373
+ val targetPixelCount = width * height
374
+ if (targetPixelCount == 0) {
376
375
  return null
377
376
  }
378
377
 
379
- val targetPixelCount = selfRect.width() * selfRect.height()
380
-
381
378
  var bestSource: SourceMap? = null
382
379
  var bestFit = Double.MAX_VALUE
383
380
 
@@ -433,8 +430,8 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
433
430
 
434
431
  val sourceToLoad = bestSource?.createGlideModel(context)
435
432
  val placeholder = bestPlaceholder?.createGlideModel(context)
436
- // We only clean the image when the source is set to null and we don't have a placeholder.
437
- if ((bestSource == null || sourceToLoad == null) && placeholder == null) {
433
+ // We only clean the image when the source is set to null and we don't have a placeholder or the view is empty.
434
+ if (width == 0 || height == 0 || (bestSource == null || sourceToLoad == null) && placeholder == null) {
438
435
  firstView.recycleView()
439
436
  secondView.recycleView()
440
437
 
@@ -621,6 +618,7 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
621
618
  companion object {
622
619
  private var requestManager: RequestManager? = null
623
620
  private var appContextRef: WeakReference<AppContext?> = WeakReference(null)
621
+ private var activityRef: WeakReference<Activity?> = WeakReference(null)
624
622
 
625
623
  fun getOrCreateRequestManager(
626
624
  appContext: AppContext,
@@ -630,13 +628,15 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
630
628
  ?: return createNewRequestManager(activity).also {
631
629
  requestManager = it
632
630
  appContextRef = WeakReference(appContext)
631
+ activityRef = WeakReference(activity)
633
632
  }
634
633
 
635
- // Request manager was created using different activity
636
- if (appContextRef.get() != appContext) {
634
+ // Request manager was created using different activity or app context
635
+ if (appContextRef.get() != appContext || activityRef.get() != activity) {
637
636
  return createNewRequestManager(activity).also {
638
637
  requestManager = it
639
638
  appContextRef = WeakReference(appContext)
639
+ activityRef = WeakReference(activity)
640
640
  }
641
641
  }
642
642
 
@@ -62,6 +62,10 @@ class GlideUrlWithCustomCacheKey(
62
62
  */
63
63
  data class GlideUrlWrapper(val glideUrl: GlideUrl) {
64
64
  var progressListener: OkHttpProgressListener? = null
65
+
66
+ override fun toString(): String {
67
+ return glideUrl.toString()
68
+ }
65
69
  }
66
70
 
67
71
  @GlideModule
@@ -13,8 +13,11 @@ class ThumbhashModelLoader : ModelLoader<GlideThumbhashModel, Bitmap> {
13
13
  override fun handles(model: GlideThumbhashModel): Boolean = true
14
14
 
15
15
  override fun buildLoadData(model: GlideThumbhashModel, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
16
- // Thumbhash might contain '/' characters
17
- val thumbhash = model.uri.pathSegments.joinToString(separator = "/")
16
+ // The URI looks like this: thumbhash:/3OcRJYB4d3h\iIeHeEh3eIhw+j2w
17
+ // ThumbHash may include slashes which could break the structure of the URL, so we replace them
18
+ // with backslashes on the JS side and revert them back to slashes here, before generating the image.
19
+ val thumbhash = model.uri.pathSegments.joinToString(separator = "/").replace("\\", "/")
20
+
18
21
  return ModelLoader.LoadData(
19
22
  ObjectKey(model),
20
23
  ThumbhashFetcher(thumbhash)
@@ -1 +1 @@
1
- {"version":3,"file":"resolveHashString.d.ts","sourceRoot":"","sources":["../../src/utils/resolveHashString.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAS7C;;;;KAIK;AACL,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAO9D;AAED;;;;KAIK;AACL,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAK/D"}
1
+ {"version":3,"file":"resolveHashString.d.ts","sourceRoot":"","sources":["../../src/utils/resolveHashString.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAS7C;;;;KAIK;AACL,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAO9D;AAED;;;;KAIK;AACL,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAO/D"}
@@ -21,7 +21,9 @@ export function resolveBlurhashString(str) {
21
21
  * @return An ImageSource representing the provided thumbhash.
22
22
  * */
23
23
  export function resolveThumbhashString(str) {
24
- const thumbhash = str.replace(/^thumbhash:\//, '');
24
+ // ThumbHash may contain slashes that could break the url when the slash is at the beginning.
25
+ // We replace slashes with backslashes to make sure we don't break the url's path.
26
+ const thumbhash = str.replace(/^thumbhash:\//, '').replace(/\//g, '\\');
25
27
  return {
26
28
  uri: hashToUri('thumbhash', thumbhash),
27
29
  };
@@ -1 +1 @@
1
- {"version":3,"file":"resolveHashString.js","sourceRoot":"","sources":["../../src/utils/resolveHashString.tsx"],"names":[],"mappings":"AAIA,SAAS,SAAS,CAAC,IAAmB,EAAE,IAAY;IAClD,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnF,OAAO,GAAG,IAAI,KAAK,eAAe,EAAE,CAAC;AACvC,CAAC;AAED;;;;KAIK;AACL,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7E,OAAO;QACL,GAAG,EAAE,SAAS,CAAC,UAAU,EAAE,QAAQ,CAAC;QACpC,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,EAAE;QAChC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,EAAE;KACnC,CAAC;AACJ,CAAC;AAED;;;;KAIK;AACL,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACnD,OAAO;QACL,GAAG,EAAE,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC;KACvC,CAAC;AACJ,CAAC","sourcesContent":["import { ImageSource } from '../Image.types';\n\ntype ImageHashType = 'blurhash' | 'thumbhash';\n\nfunction hashToUri(type: ImageHashType, hash: string): string {\n const encodedBlurhash = encodeURI(hash).replace(/#/g, '%23').replace(/\\?/g, '%3F');\n return `${type}:/${encodedBlurhash}`;\n}\n\n/**\n * Converts a blurhash string (`blurhash:/<hash>/<width>/<height>` or <hash>/<width>/<height>) into an `ImageSource`.\n *\n * @return An ImageSource representing the provided blurhash.\n * */\nexport function resolveBlurhashString(str: string): ImageSource {\n const [blurhash, width, height] = str.replace(/^blurhash:\\//, '').split('/');\n return {\n uri: hashToUri('blurhash', blurhash),\n width: parseInt(width, 10) || 16,\n height: parseInt(height, 10) || 16,\n };\n}\n\n/**\n * Converts a thumbhash string (`thumbhash:/<hash>` or `<hash>`) into an `ImageSource`.\n *\n * @return An ImageSource representing the provided thumbhash.\n * */\nexport function resolveThumbhashString(str: string): ImageSource {\n const thumbhash = str.replace(/^thumbhash:\\//, '');\n return {\n uri: hashToUri('thumbhash', thumbhash),\n };\n}\n"]}
1
+ {"version":3,"file":"resolveHashString.js","sourceRoot":"","sources":["../../src/utils/resolveHashString.tsx"],"names":[],"mappings":"AAIA,SAAS,SAAS,CAAC,IAAmB,EAAE,IAAY;IAClD,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnF,OAAO,GAAG,IAAI,KAAK,eAAe,EAAE,CAAC;AACvC,CAAC;AAED;;;;KAIK;AACL,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7E,OAAO;QACL,GAAG,EAAE,SAAS,CAAC,UAAU,EAAE,QAAQ,CAAC;QACpC,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,EAAE;QAChC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,EAAE;KACnC,CAAC;AACJ,CAAC;AAED;;;;KAIK;AACL,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,6FAA6F;IAC7F,kFAAkF;IAClF,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACxE,OAAO;QACL,GAAG,EAAE,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC;KACvC,CAAC;AACJ,CAAC","sourcesContent":["import { ImageSource } from '../Image.types';\n\ntype ImageHashType = 'blurhash' | 'thumbhash';\n\nfunction hashToUri(type: ImageHashType, hash: string): string {\n const encodedBlurhash = encodeURI(hash).replace(/#/g, '%23').replace(/\\?/g, '%3F');\n return `${type}:/${encodedBlurhash}`;\n}\n\n/**\n * Converts a blurhash string (`blurhash:/<hash>/<width>/<height>` or <hash>/<width>/<height>) into an `ImageSource`.\n *\n * @return An ImageSource representing the provided blurhash.\n * */\nexport function resolveBlurhashString(str: string): ImageSource {\n const [blurhash, width, height] = str.replace(/^blurhash:\\//, '').split('/');\n return {\n uri: hashToUri('blurhash', blurhash),\n width: parseInt(width, 10) || 16,\n height: parseInt(height, 10) || 16,\n };\n}\n\n/**\n * Converts a thumbhash string (`thumbhash:/<hash>` or `<hash>`) into an `ImageSource`.\n *\n * @return An ImageSource representing the provided thumbhash.\n * */\nexport function resolveThumbhashString(str: string): ImageSource {\n // ThumbHash may contain slashes that could break the url when the slash is at the beginning.\n // We replace slashes with backslashes to make sure we don't break the url's path.\n const thumbhash = str.replace(/^thumbhash:\\//, '').replace(/\\//g, '\\\\');\n return {\n uri: hashToUri('thumbhash', thumbhash),\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"AnimationManager.d.ts","sourceRoot":"","sources":["../../src/web/AnimationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,KAAK,SAAS,GAAG;IACf,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,mBAAmB,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM;IACX,cAAc,EAAE,CACd,WAAW,EAAE,WAAW,CAAC,SAAS,CAAC,KAChC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,aAAa,KAAK,KAAK,CAAC,YAAY;CAC3E,CAAC;AAgDF,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS;;;;;;;;;;;;;;;;SA+BvF;AAUD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,QAAQ,EAAE,cAAc,EACxB,OAAO,EACP,UAAU,EACV,YAAY,GACb,EAAE;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C,eAsGA"}
1
+ {"version":3,"file":"AnimationManager.d.ts","sourceRoot":"","sources":["../../src/web/AnimationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,KAAK,SAAS,GAAG;IACf,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,mBAAmB,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM;IACX,cAAc,EAAE,CACd,WAAW,EAAE,WAAW,CAAC,SAAS,CAAC,KAChC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,aAAa,KAAK,KAAK,CAAC,YAAY;CAC3E,CAAC;AAgDF,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS;;;;;;;;;;;;;;;;SA+BvF;AAUD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,QAAQ,EAAE,cAAc,EACxB,OAAO,EACP,UAAU,EACV,YAAY,GACb,EAAE;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C,eA4GA"}
@@ -86,9 +86,14 @@ export default function AnimationManager({ children: renderFunction, initial, tr
86
86
  }
87
87
  const existingNodeIndex = n.findIndex((node) => node.animationKey === newNode.animationKey);
88
88
  if (existingNodeIndex >= 0) {
89
- const copy = [...n];
90
- copy.splice(existingNodeIndex, 1, newNode);
91
- return copy;
89
+ if (animation) {
90
+ return n.map((n2) => n2.animationKey === newNode.animationKey
91
+ ? { ...newNode, status: 'in' }
92
+ : { ...n2, status: 'out' });
93
+ }
94
+ else {
95
+ return [{ ...newNode, status: 'in' }];
96
+ }
92
97
  }
93
98
  return [...n, newNode];
94
99
  });
@@ -1 +1 @@
1
- {"version":3,"file":"AnimationManager.js","sourceRoot":"","sources":["../../src/web/AnimationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAkB1B,MAAM,oBAAoB,GAAgC;IACxD,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;IACjB,eAAe;IACf,kBAAkB;CACnB,CAAC;AAIF,SAAS,uBAAuB,CAAC,IAAiC,EAAE,aAA0B;IAC5F,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACjC,IAAI,CAAC,IAAI,EAAE;YACT,OAAO,IAAI,CAAC;SACb;QACD,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,GAAG,IAAI,CAAC;QAC5C,oCAAoC;QACpC,OAAO;YACL,YAAY;YACZ,gBAAgB,EAAE,cAAc;YAChC,MAAM,EAAE,CAAC,aAAa,IAAI,SAAS,CAAe;SACnD,CAAC;IACJ,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,kCAAkC,CACzC,cAAyC,EACzC,cAAyC;IAEzC,IAAI,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;QACpC,IAAI,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;YACpC,OAAO,aAAa,CAAC;SACtB;QACD,OAAO,QAAQ,CAAC;KACjB;IACD,OAAO,cAAc,IAAI,IAAI,CAAC;AAChC,CAAC;AAED,SAAS,sBAAsB,CAAC,MAAiC;IAC/D,IAAI,oBAAoB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;QACzC,OAAO,MAAM,CAAC;KACf;IACD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,UAA8C;IACtF,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE;QACzB,OAAO,IAAI,CAAC;KACb;IACD,MAAM,cAAc,GAAG,sBAAsB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjE,IAAI,CAAC,cAAc,EAAE;QACnB,OAAO;YACL,aAAa,EAAE,EAAE;YACjB,cAAc,EAAE,EAAE;YAClB,eAAe,EAAE,EAAE;YACnB,cAAc,EAAE,EAAE;YAClB,cAAc,EAAE,QAAQ;YACxB,cAAc,EAAE,EAAE;YAClB,QAAQ,EAAE,CAAC;SACZ,CAAC;KACH;IAED,MAAM,cAAc,GAAG,kCAAkC,CAAC,cAAc,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC7F,MAAM,WAAW,GAAG,gBAAgB,cAAc,EAAE,CAAC;IAErD,OAAO;QACL,aAAa,EAAE,GAAG,cAAc,QAAQ;QACxC,cAAc,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,GAAG,cAAc,SAAS,EAAE,WAAW,CAAC,CAAC,IAAI,CAC7F,GAAG,CACJ;QACD,eAAe,EAAE,CAAC,cAAc,EAAE,GAAG,cAAc,MAAM,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACjF,cAAc,EAAE,GAAG,cAAc,YAAY;QAC7C,cAAc;QACd,cAAc;QACd,QAAQ,EAAE,UAAU,EAAE,QAAQ,IAAI,CAAC;KACpC,CAAC;AACJ,CAAC;AAUD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,QAAQ,EAAE,cAAc,EACxB,OAAO,EACP,UAAU,EACV,YAAY,GAMb;IACC,MAAM,SAAS,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IAExD,MAAM,WAAW,GAAG,uBAAuB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAE/D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CACtC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CACjC,CAAC;IAEF,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAS,YAAY,IAAI,EAAE,CAAC,CAAC;IAC3F,IAAI,gBAAgB,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE;QAC7C,mBAAmB,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;QACxC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;KAC5C;IAED,MAAM,gCAAgC,GAAG,CAAC,GAAY,EAAE,EAAE;QACxD,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CACb,CAAC,CAAC,MAAM,CACN,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YACzC,IAAI,CAAC,MAAM,KAAK,IAAI;YACpB,IAAI,CAAC,MAAM,KAAK,QAAQ,CAC3B,CACF,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,uBAAuB,CAAC,cAAc,CAAC,CAAC;IAExD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE;YACb,IAAI,CAAC,OAAO,EAAE;gBACZ,OAAO,CAAC,CAAC;aACV;YACD,MAAM,iBAAiB,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;YAC5F,IAAI,iBAAiB,IAAI,CAAC,EAAE;gBAC1B,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;gBAC3C,OAAO,IAAI,CAAC;aACb;YACD,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,SAAS,qBAAqB,CAAC,IAA0B;QACvD,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,YAAY,EAAE;YAC3C,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC;gBACvB,OAAO,EAAE,GAAG,EAAE;oBACZ,IAAI,SAAS,EAAE;wBACb,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CACjB,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CACrF,CAAC;qBACH;yBAAM;wBACL,QAAQ,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;qBACvC;gBACH,CAAC;gBACD,mBAAmB,EAAE,GAAG,EAAE;oBACxB,QAAQ,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBACxC,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE;oBACZ,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxF,CAAC;aACF,CAAC,CAAC;SACJ;QACD,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,YAAY,EAAE;YACtC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;gBAChB,mBAAmB,EAAE,GAAG,EAAE;oBACxB,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,EAAE;wBACzB,gCAAgC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;qBACrD;gBACH,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE;oBACZ,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxF,CAAC;aACF,CAAC,CAAC;SACJ;QACD,OAAO,IAAI,CAAC,gBAAgB,CAAC;YAC3B,mBAAmB,EAAE,GAAG,EAAE;gBACxB,gCAAgC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACtD,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG;QACb,kBAAkB,EAAE,GAAG,SAAS,EAAE,QAAQ,IAAI,CAAC,IAAI;QACnD,wBAAwB,EAAE,SAAS,EAAE,cAAc,IAAI,QAAQ;KAChE,CAAC;IACF,MAAM,OAAO,GAAG;QACd,EAAE,EAAE,SAAS,EAAE,cAAc;QAC7B,GAAG,EAAE,SAAS,EAAE,eAAe;QAC/B,OAAO,EAAE,SAAS,EAAE,aAAa;KAClC,CAAC;IAEF,OAAO,CACL,0CACG,CAAC,GAAG,KAAK,CAAC;SACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC;SACrC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACV,6BAAK,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY,IAC3D,qBAAqB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAChD,CACP,CAAC,CACH,CACJ,CAAC;AACJ,CAAC","sourcesContent":["import React from 'react';\n\nimport { ImageTransition } from '../Image.types';\n\ntype Callbacks = {\n onReady?: (() => void) | null;\n onAnimationFinished?: (() => void) | null;\n onMount?: (() => void) | null;\n onError?: (() => void) | null;\n};\n\nexport type AnimationManagerNode = [\n key: string,\n renderFunction: (\n renderProps: NonNullable<Callbacks>\n ) => (className: string, style: React.CSSProperties) => React.ReactElement\n];\n\nconst SUPPORTED_ANIMATIONS: ImageTransition['effect'][] = [\n 'cross-dissolve',\n 'flip-from-left',\n 'flip-from-right',\n 'flip-from-top',\n 'flip-from-bottom',\n];\n\ntype NodeStatus = 'mounted' | 'in' | 'active' | 'out' | 'errored';\n\nfunction useAnimationManagerNode(node: AnimationManagerNode | null, initialStatus?: NodeStatus) {\n const newNode = React.useMemo(() => {\n if (!node) {\n return null;\n }\n const [animationKey, renderFunction] = node;\n // key, ReactElement, ref, callbacks\n return {\n animationKey,\n persistedElement: renderFunction,\n status: (initialStatus || 'mounted') as NodeStatus,\n };\n }, [node?.[0]]);\n return newNode;\n}\n\nfunction validateTimingFunctionForAnimation(\n animationClass: ImageTransition['effect'],\n timingFunction: ImageTransition['timing']\n) {\n if (animationClass?.includes('flip')) {\n if (timingFunction?.includes('ease')) {\n return 'ease-in-out';\n }\n return 'linear';\n }\n return timingFunction || null;\n}\n\nfunction validateAnimationClass(effect: ImageTransition['effect']) {\n if (SUPPORTED_ANIMATIONS.includes(effect)) {\n return effect;\n }\n return 'cross-dissolve';\n}\n\nexport function getAnimatorFromTransition(transition: ImageTransition | null | undefined) {\n if (!transition?.duration) {\n return null;\n }\n const animationClass = validateAnimationClass(transition.effect);\n if (!animationClass) {\n return {\n startingClass: '',\n animateInClass: '',\n animateOutClass: '',\n containerClass: '',\n timingFunction: 'linear',\n animationClass: '',\n duration: 0,\n };\n }\n\n const timingFunction = validateTimingFunctionForAnimation(animationClass, transition.timing);\n const timingClass = `image-timing-${timingFunction}`;\n\n return {\n startingClass: `${animationClass}-start`,\n animateInClass: [animationClass, 'transitioning', `${animationClass}-active`, timingClass].join(\n ' '\n ),\n animateOutClass: [animationClass, `${animationClass}-end`, timingClass].join(' '),\n containerClass: `${animationClass}-container`,\n timingFunction,\n animationClass,\n duration: transition?.duration || 0,\n };\n}\n\ntype MountedAnimationNode = {\n animationKey: string;\n persistedElement: (\n renderProps: Callbacks\n ) => (className: string, style: React.CSSProperties) => React.ReactElement;\n status: NodeStatus;\n};\n\nexport default function AnimationManager({\n children: renderFunction,\n initial,\n transition,\n recyclingKey,\n}: {\n children: AnimationManagerNode;\n initial: AnimationManagerNode | null;\n transition: ImageTransition | null | undefined;\n recyclingKey?: string | null | undefined;\n}) {\n const animation = getAnimatorFromTransition(transition);\n\n const initialNode = useAnimationManagerNode(initial, 'active');\n\n const [nodes, setNodes] = React.useState<MountedAnimationNode[]>(\n initialNode ? [initialNode] : []\n );\n\n const [prevRecyclingKey, setPrevRecyclingKey] = React.useState<string>(recyclingKey ?? '');\n if (prevRecyclingKey !== (recyclingKey ?? '')) {\n setPrevRecyclingKey(recyclingKey ?? '');\n setNodes(initialNode ? [initialNode] : []);\n }\n\n const removeAllNodesOfKeyExceptShowing = (key?: string) => {\n setNodes((n) =>\n n.filter(\n (node) =>\n (key ? node.animationKey !== key : false) ||\n node.status === 'in' ||\n node.status === 'active'\n )\n );\n };\n\n const newNode = useAnimationManagerNode(renderFunction);\n\n React.useEffect(() => {\n setNodes((n) => {\n if (!newNode) {\n return n;\n }\n const existingNodeIndex = n.findIndex((node) => node.animationKey === newNode.animationKey);\n if (existingNodeIndex >= 0) {\n const copy = [...n];\n copy.splice(existingNodeIndex, 1, newNode);\n return copy;\n }\n return [...n, newNode];\n });\n }, [newNode]);\n\n function wrapNodeWithCallbacks(node: MountedAnimationNode) {\n if (renderFunction[0] === node.animationKey) {\n return renderFunction[1]({\n onReady: () => {\n if (animation) {\n setNodes((nodes) =>\n nodes.map((n) => (n === newNode ? { ...n, status: 'in' } : { ...n, status: 'out' }))\n );\n } else {\n setNodes([{ ...node, status: 'in' }]);\n }\n },\n onAnimationFinished: () => {\n setNodes([{ ...node, status: 'in' }]);\n },\n onError: () => {\n setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));\n },\n });\n }\n if (initial?.[0] === node.animationKey) {\n return initial[1]({\n onAnimationFinished: () => {\n if (node.status === 'out') {\n removeAllNodesOfKeyExceptShowing(node.animationKey);\n }\n },\n onError: () => {\n setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));\n },\n });\n }\n return node.persistedElement({\n onAnimationFinished: () => {\n removeAllNodesOfKeyExceptShowing(node.animationKey);\n },\n });\n }\n const styles = {\n transitionDuration: `${animation?.duration || 0}ms`,\n transitionTimingFunction: animation?.timingFunction || 'linear',\n };\n const classes = {\n in: animation?.animateInClass,\n out: animation?.animateOutClass,\n mounted: animation?.startingClass,\n };\n\n return (\n <>\n {[...nodes]\n .filter((n) => n.status !== 'errored')\n .map((n) => (\n <div className={animation?.containerClass} key={n.animationKey}>\n {wrapNodeWithCallbacks(n)(classes[n.status], styles)}\n </div>\n ))}\n </>\n );\n}\n"]}
1
+ {"version":3,"file":"AnimationManager.js","sourceRoot":"","sources":["../../src/web/AnimationManager.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport { ImageTransition } from '../Image.types';\n\ntype Callbacks = {\n onReady?: (() => void) | null;\n onAnimationFinished?: (() => void) | null;\n onMount?: (() => void) | null;\n onError?: (() => void) | null;\n};\n\nexport type AnimationManagerNode = [\n key: string,\n renderFunction: (\n renderProps: NonNullable<Callbacks>\n ) => (className: string, style: React.CSSProperties) => React.ReactElement\n];\n\nconst SUPPORTED_ANIMATIONS: ImageTransition['effect'][] = [\n 'cross-dissolve',\n 'flip-from-left',\n 'flip-from-right',\n 'flip-from-top',\n 'flip-from-bottom',\n];\n\ntype NodeStatus = 'mounted' | 'in' | 'active' | 'out' | 'errored';\n\nfunction useAnimationManagerNode(node: AnimationManagerNode | null, initialStatus?: NodeStatus) {\n const newNode = React.useMemo(() => {\n if (!node) {\n return null;\n }\n const [animationKey, renderFunction] = node;\n // key, ReactElement, ref, callbacks\n return {\n animationKey,\n persistedElement: renderFunction,\n status: (initialStatus || 'mounted') as NodeStatus,\n };\n }, [node?.[0]]);\n return newNode;\n}\n\nfunction validateTimingFunctionForAnimation(\n animationClass: ImageTransition['effect'],\n timingFunction: ImageTransition['timing']\n) {\n if (animationClass?.includes('flip')) {\n if (timingFunction?.includes('ease')) {\n return 'ease-in-out';\n }\n return 'linear';\n }\n return timingFunction || null;\n}\n\nfunction validateAnimationClass(effect: ImageTransition['effect']) {\n if (SUPPORTED_ANIMATIONS.includes(effect)) {\n return effect;\n }\n return 'cross-dissolve';\n}\n\nexport function getAnimatorFromTransition(transition: ImageTransition | null | undefined) {\n if (!transition?.duration) {\n return null;\n }\n const animationClass = validateAnimationClass(transition.effect);\n if (!animationClass) {\n return {\n startingClass: '',\n animateInClass: '',\n animateOutClass: '',\n containerClass: '',\n timingFunction: 'linear',\n animationClass: '',\n duration: 0,\n };\n }\n\n const timingFunction = validateTimingFunctionForAnimation(animationClass, transition.timing);\n const timingClass = `image-timing-${timingFunction}`;\n\n return {\n startingClass: `${animationClass}-start`,\n animateInClass: [animationClass, 'transitioning', `${animationClass}-active`, timingClass].join(\n ' '\n ),\n animateOutClass: [animationClass, `${animationClass}-end`, timingClass].join(' '),\n containerClass: `${animationClass}-container`,\n timingFunction,\n animationClass,\n duration: transition?.duration || 0,\n };\n}\n\ntype MountedAnimationNode = {\n animationKey: string;\n persistedElement: (\n renderProps: Callbacks\n ) => (className: string, style: React.CSSProperties) => React.ReactElement;\n status: NodeStatus;\n};\n\nexport default function AnimationManager({\n children: renderFunction,\n initial,\n transition,\n recyclingKey,\n}: {\n children: AnimationManagerNode;\n initial: AnimationManagerNode | null;\n transition: ImageTransition | null | undefined;\n recyclingKey?: string | null | undefined;\n}) {\n const animation = getAnimatorFromTransition(transition);\n\n const initialNode = useAnimationManagerNode(initial, 'active');\n\n const [nodes, setNodes] = React.useState<MountedAnimationNode[]>(\n initialNode ? [initialNode] : []\n );\n\n const [prevRecyclingKey, setPrevRecyclingKey] = React.useState<string>(recyclingKey ?? '');\n if (prevRecyclingKey !== (recyclingKey ?? '')) {\n setPrevRecyclingKey(recyclingKey ?? '');\n setNodes(initialNode ? [initialNode] : []);\n }\n\n const removeAllNodesOfKeyExceptShowing = (key?: string) => {\n setNodes((n) =>\n n.filter(\n (node) =>\n (key ? node.animationKey !== key : false) ||\n node.status === 'in' ||\n node.status === 'active'\n )\n );\n };\n\n const newNode = useAnimationManagerNode(renderFunction);\n\n React.useEffect(() => {\n setNodes((n) => {\n if (!newNode) {\n return n;\n }\n const existingNodeIndex = n.findIndex((node) => node.animationKey === newNode.animationKey);\n if (existingNodeIndex >= 0) {\n if (animation) {\n return n.map((n2) =>\n n2.animationKey === newNode.animationKey\n ? { ...newNode, status: 'in' }\n : { ...n2, status: 'out' }\n );\n } else {\n return [{ ...newNode, status: 'in' }];\n }\n }\n return [...n, newNode];\n });\n }, [newNode]);\n\n function wrapNodeWithCallbacks(node: MountedAnimationNode) {\n if (renderFunction[0] === node.animationKey) {\n return renderFunction[1]({\n onReady: () => {\n if (animation) {\n setNodes((nodes) =>\n nodes.map((n) => (n === newNode ? { ...n, status: 'in' } : { ...n, status: 'out' }))\n );\n } else {\n setNodes([{ ...node, status: 'in' }]);\n }\n },\n onAnimationFinished: () => {\n setNodes([{ ...node, status: 'in' }]);\n },\n onError: () => {\n setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));\n },\n });\n }\n if (initial?.[0] === node.animationKey) {\n return initial[1]({\n onAnimationFinished: () => {\n if (node.status === 'out') {\n removeAllNodesOfKeyExceptShowing(node.animationKey);\n }\n },\n onError: () => {\n setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));\n },\n });\n }\n return node.persistedElement({\n onAnimationFinished: () => {\n removeAllNodesOfKeyExceptShowing(node.animationKey);\n },\n });\n }\n const styles = {\n transitionDuration: `${animation?.duration || 0}ms`,\n transitionTimingFunction: animation?.timingFunction || 'linear',\n };\n const classes = {\n in: animation?.animateInClass,\n out: animation?.animateOutClass,\n mounted: animation?.startingClass,\n };\n\n return (\n <>\n {[...nodes]\n .filter((n) => n.status !== 'errored')\n .map((n) => (\n <div className={animation?.containerClass} key={n.animationKey}>\n {wrapNodeWithCallbacks(n)(classes[n.status], styles)}\n </div>\n ))}\n </>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ImageWrapper.d.ts","sourceRoot":"","sources":["../../src/web/ImageWrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAE,cAAc,EAA2B,MAAM,OAAO,CAAC;AAEtF,OAAO,EACL,0BAA0B,EAG1B,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAgDxB,QAAA,MAAM,YAAY;;;2BAeS,eAAe,gBAAgB,EAAE,KAAK,CAAC,KAAK,IAAI;;oBAChC,WAAW,GAAG,IAAI;cAAO,IAAI;kCACtC,IAAI;0BACZ,IAAI;;;;;WAKjB,aAAa;;;;0CAwDzB,CAAC;AACF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ImageWrapper.d.ts","sourceRoot":"","sources":["../../src/web/ImageWrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAE,cAAc,EAA2B,MAAM,OAAO,CAAC;AAEtF,OAAO,EACL,0BAA0B,EAG1B,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAgDxB,QAAA,MAAM,YAAY;;;2BAeS,eAAe,gBAAgB,EAAE,KAAK,CAAC,KAAK,IAAI;;oBAChC,WAAW,GAAG,IAAI;cAAO,IAAI;kCACtC,IAAI;0BACZ,IAAI;;;;;WAKjB,aAAa;;;;0CA+DzB,CAAC;AACF,eAAe,YAAY,CAAC"}
@@ -60,7 +60,15 @@ const ImageWrapper = React.forwardRef(({ source, events, contentPosition, hashPl
60
60
  // @ts-ignore
61
61
  // eslint-disable-next-line react/no-unknown-property
62
62
  fetchpriority: getFetchPriorityFromImagePriority(priority || 'normal'), onLoad: (event) => {
63
- events?.onLoad?.forEach((e) => e?.(event));
63
+ if (typeof window !== 'undefined') {
64
+ // this ensures the animation will run, since the starting class is applied at least 1 frame before the target class set in the onLoad event callback
65
+ window.requestAnimationFrame(() => {
66
+ events?.onLoad?.forEach((e) => e?.(event));
67
+ });
68
+ }
69
+ else {
70
+ events?.onLoad?.forEach((e) => e?.(event));
71
+ }
64
72
  }, onTransitionEnd: () => events?.onTransitionEnd?.forEach((e) => e?.()), onError: () => events?.onError?.forEach((e) => e?.({ source: source || null })) }));
65
73
  });
66
74
  export default ImageWrapper;
@@ -1 +1 @@
1
- {"version":3,"file":"ImageWrapper.js","sourceRoot":"","sources":["../../src/web/ImageWrapper.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { CSSProperties, SyntheticEvent, useEffect, Ref, useMemo } from 'react';\n\nimport {\n ImageContentPositionObject,\n ImageContentPositionValue,\n ImageNativeProps,\n ImageSource,\n} from '../Image.types';\nimport { useBlurhash } from '../utils/blurhash/useBlurhash';\nimport { isBlurhashString, isThumbhashString } from '../utils/resolveSources';\nimport { thumbHashStringToDataURL } from '../utils/thumbhash/thumbhash';\n\nfunction ensureUnit(value: string | number) {\n const trimmedValue = String(value).trim();\n if (trimmedValue.endsWith('%')) {\n return trimmedValue;\n }\n return `${trimmedValue}px`;\n}\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\n\nfunction getObjectPositionFromContentPositionObject(\n contentPosition?: ImageContentPositionObject\n): string {\n const resolvedPosition = { ...contentPosition } as Record<\n KeysOfUnion<ImageContentPositionObject>,\n ImageContentPositionValue\n >;\n if (!resolvedPosition) {\n return '50% 50%';\n }\n if (resolvedPosition.top == null && resolvedPosition.bottom == null) {\n resolvedPosition.top = '50%';\n }\n if (resolvedPosition.left == null && resolvedPosition.right == null) {\n resolvedPosition.left = '50%';\n }\n\n return (\n ['top', 'bottom', 'left', 'right']\n .map((key) => {\n if (key in resolvedPosition) {\n return `${key} ${ensureUnit(resolvedPosition[key])}`;\n }\n return '';\n })\n .join(' ') || '50% 50%'\n );\n}\n\nfunction getFetchPriorityFromImagePriority(priority: ImageNativeProps['priority'] = 'normal') {\n return priority && ['low', 'high'].includes(priority) ? priority : 'auto';\n}\n\nconst ImageWrapper = React.forwardRef(\n (\n {\n source,\n events,\n contentPosition,\n hashPlaceholderContentPosition,\n priority,\n style,\n hashPlaceholderStyle,\n className,\n accessibilityLabel,\n }: {\n source?: ImageSource | null;\n events?: {\n onLoad?: (((event: SyntheticEvent<HTMLImageElement, Event>) => void) | undefined | null)[];\n onError?: ((({ source }: { source: ImageSource | null }) => void) | undefined | null)[];\n onTransitionEnd?: ((() => void) | undefined | null)[];\n onMount?: ((() => void) | undefined | null)[];\n };\n contentPosition?: ImageContentPositionObject;\n hashPlaceholderContentPosition?: ImageContentPositionObject;\n priority?: string | null;\n style: CSSProperties;\n hashPlaceholderStyle?: CSSProperties;\n className?: string;\n accessibilityLabel?: string;\n },\n ref: Ref<HTMLImageElement>\n ) => {\n useEffect(() => {\n events?.onMount?.forEach((e) => e?.());\n }, []);\n const isBlurhash = isBlurhashString(source?.uri || '');\n const isThumbhash = isThumbhashString(source?.uri || '');\n const isHash = isBlurhash || isThumbhash;\n\n // Thumbhash uri always has to start with 'thumbhash:/'\n const thumbhash = source?.uri?.replace(/thumbhash:\\//, '');\n const thumbhashUri = useMemo(\n () => (isThumbhash ? thumbHashStringToDataURL(thumbhash ?? '') : null),\n [thumbhash]\n );\n\n const blurhashUri = useBlurhash(isBlurhash ? source?.uri : null, source?.width, source?.height);\n const objectPosition = getObjectPositionFromContentPositionObject(\n isHash ? hashPlaceholderContentPosition : contentPosition\n );\n\n const uri = isHash ? blurhashUri ?? thumbhashUri : source?.uri;\n if (!uri) return null;\n return (\n <img\n ref={ref}\n alt={accessibilityLabel}\n className={className}\n src={uri || undefined}\n key={source?.uri}\n style={{\n width: '100%',\n height: '100%',\n position: 'absolute',\n left: 0,\n right: 0,\n objectPosition,\n ...style,\n ...(isHash ? hashPlaceholderStyle : {}),\n }}\n // @ts-ignore\n // eslint-disable-next-line react/no-unknown-property\n fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')}\n onLoad={(event) => {\n events?.onLoad?.forEach((e) => e?.(event));\n }}\n onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())}\n onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))}\n />\n );\n }\n);\nexport default ImageWrapper;\n"]}
1
+ {"version":3,"file":"ImageWrapper.js","sourceRoot":"","sources":["../../src/web/ImageWrapper.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { CSSProperties, SyntheticEvent, useEffect, Ref, useMemo } from 'react';\n\nimport {\n ImageContentPositionObject,\n ImageContentPositionValue,\n ImageNativeProps,\n ImageSource,\n} from '../Image.types';\nimport { useBlurhash } from '../utils/blurhash/useBlurhash';\nimport { isBlurhashString, isThumbhashString } from '../utils/resolveSources';\nimport { thumbHashStringToDataURL } from '../utils/thumbhash/thumbhash';\n\nfunction ensureUnit(value: string | number) {\n const trimmedValue = String(value).trim();\n if (trimmedValue.endsWith('%')) {\n return trimmedValue;\n }\n return `${trimmedValue}px`;\n}\n\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\n\nfunction getObjectPositionFromContentPositionObject(\n contentPosition?: ImageContentPositionObject\n): string {\n const resolvedPosition = { ...contentPosition } as Record<\n KeysOfUnion<ImageContentPositionObject>,\n ImageContentPositionValue\n >;\n if (!resolvedPosition) {\n return '50% 50%';\n }\n if (resolvedPosition.top == null && resolvedPosition.bottom == null) {\n resolvedPosition.top = '50%';\n }\n if (resolvedPosition.left == null && resolvedPosition.right == null) {\n resolvedPosition.left = '50%';\n }\n\n return (\n ['top', 'bottom', 'left', 'right']\n .map((key) => {\n if (key in resolvedPosition) {\n return `${key} ${ensureUnit(resolvedPosition[key])}`;\n }\n return '';\n })\n .join(' ') || '50% 50%'\n );\n}\n\nfunction getFetchPriorityFromImagePriority(priority: ImageNativeProps['priority'] = 'normal') {\n return priority && ['low', 'high'].includes(priority) ? priority : 'auto';\n}\n\nconst ImageWrapper = React.forwardRef(\n (\n {\n source,\n events,\n contentPosition,\n hashPlaceholderContentPosition,\n priority,\n style,\n hashPlaceholderStyle,\n className,\n accessibilityLabel,\n }: {\n source?: ImageSource | null;\n events?: {\n onLoad?: (((event: SyntheticEvent<HTMLImageElement, Event>) => void) | undefined | null)[];\n onError?: ((({ source }: { source: ImageSource | null }) => void) | undefined | null)[];\n onTransitionEnd?: ((() => void) | undefined | null)[];\n onMount?: ((() => void) | undefined | null)[];\n };\n contentPosition?: ImageContentPositionObject;\n hashPlaceholderContentPosition?: ImageContentPositionObject;\n priority?: string | null;\n style: CSSProperties;\n hashPlaceholderStyle?: CSSProperties;\n className?: string;\n accessibilityLabel?: string;\n },\n ref: Ref<HTMLImageElement>\n ) => {\n useEffect(() => {\n events?.onMount?.forEach((e) => e?.());\n }, []);\n const isBlurhash = isBlurhashString(source?.uri || '');\n const isThumbhash = isThumbhashString(source?.uri || '');\n const isHash = isBlurhash || isThumbhash;\n\n // Thumbhash uri always has to start with 'thumbhash:/'\n const thumbhash = source?.uri?.replace(/thumbhash:\\//, '');\n const thumbhashUri = useMemo(\n () => (isThumbhash ? thumbHashStringToDataURL(thumbhash ?? '') : null),\n [thumbhash]\n );\n\n const blurhashUri = useBlurhash(isBlurhash ? source?.uri : null, source?.width, source?.height);\n const objectPosition = getObjectPositionFromContentPositionObject(\n isHash ? hashPlaceholderContentPosition : contentPosition\n );\n\n const uri = isHash ? blurhashUri ?? thumbhashUri : source?.uri;\n if (!uri) return null;\n return (\n <img\n ref={ref}\n alt={accessibilityLabel}\n className={className}\n src={uri || undefined}\n key={source?.uri}\n style={{\n width: '100%',\n height: '100%',\n position: 'absolute',\n left: 0,\n right: 0,\n objectPosition,\n ...style,\n ...(isHash ? hashPlaceholderStyle : {}),\n }}\n // @ts-ignore\n // eslint-disable-next-line react/no-unknown-property\n fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')}\n onLoad={(event) => {\n if (typeof window !== 'undefined') {\n // this ensures the animation will run, since the starting class is applied at least 1 frame before the target class set in the onLoad event callback\n window.requestAnimationFrame(() => {\n events?.onLoad?.forEach((e) => e?.(event));\n });\n } else {\n events?.onLoad?.forEach((e) => e?.(event));\n }\n }}\n onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())}\n onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))}\n />\n );\n }\n);\nexport default ImageWrapper;\n"]}
@@ -78,7 +78,8 @@ private func assetLocalIdentifier(fromUrl url: URL) -> String? {
78
78
  */
79
79
  private func isPhotoLibraryStatusAuthorized() -> Bool {
80
80
  if #available(iOS 14, *) {
81
- return PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized
81
+ let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
82
+ return status == .authorized || status == .limited
82
83
  } else {
83
84
  return PHPhotoLibrary.authorizationStatus() == .authorized
84
85
  }
@@ -21,9 +21,10 @@ class ThumbhashLoader: NSObject, SDImageLoader {
21
21
  return nil
22
22
  }
23
23
 
24
- // The URI looks like this: thumbhash:/3OcRJYB4d3h/iIeHeEh3eIhw+j2w
25
- // the "thumbhash:/" part has to be skipped, but the hash can contain other '/' characters, which need to be included
26
- var thumbhash = url.pathComponents[1..<url.pathComponents.count].joined(separator: "/")
24
+ // The URI looks like this: thumbhash:/3OcRJYB4d3h\iIeHeEh3eIhw+j2w
25
+ // ThumbHash may include slashes which could break the structure of the URL, so we replace them
26
+ // with backslashes on the JS side and revert them back to slashes here, before generating the image.
27
+ var thumbhash = (url.pathComponents[1] ?? "").replacingOccurrences(of: "\\", with: "/")
27
28
 
28
29
  // Thumbhashes with transparency cause the conversion to data to fail, padding the thumbhash string to correct length fixes that
29
30
  let remainder = thumbhash.count % 4
@@ -31,7 +32,7 @@ class ThumbhashLoader: NSObject, SDImageLoader {
31
32
  thumbhash = thumbhash.padding(toLength: thumbhash.count + 4 - remainder, withPad: "=", startingAt: 0)
32
33
  }
33
34
 
34
- guard let thumbhashData = Data(base64Encoded: thumbhash, options: .ignoreUnknownCharacters) else {
35
+ guard !thumbhash.isEmpty, let thumbhashData = Data(base64Encoded: thumbhash, options: .ignoreUnknownCharacters) else {
35
36
  let error = makeNSError(description: "URL provided to ThumbhashLoader is invalid")
36
37
  completedBlock?(nil, nil, error, false)
37
38
  return nil
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-image",
3
3
  "title": "Expo Image",
4
- "version": "1.2.0",
4
+ "version": "1.2.2",
5
5
  "description": "A cross-platform, performant image component for React Native and Expo with Web support",
6
6
  "main": "build/index.js",
7
7
  "types": "build/index.d.ts",
@@ -33,5 +33,5 @@
33
33
  "peerDependencies": {
34
34
  "expo": "*"
35
35
  },
36
- "gitHead": "cbd598a92d6b14a61b429cf0f900a44ee73f0766"
36
+ "gitHead": "e7ef246d8e7c274cc6ce67eee4d0d764bd3f042f"
37
37
  }
@@ -27,7 +27,9 @@ export function resolveBlurhashString(str: string): ImageSource {
27
27
  * @return An ImageSource representing the provided thumbhash.
28
28
  * */
29
29
  export function resolveThumbhashString(str: string): ImageSource {
30
- const thumbhash = str.replace(/^thumbhash:\//, '');
30
+ // ThumbHash may contain slashes that could break the url when the slash is at the beginning.
31
+ // We replace slashes with backslashes to make sure we don't break the url's path.
32
+ const thumbhash = str.replace(/^thumbhash:\//, '').replace(/\//g, '\\');
31
33
  return {
32
34
  uri: hashToUri('thumbhash', thumbhash),
33
35
  };
@@ -148,9 +148,15 @@ export default function AnimationManager({
148
148
  }
149
149
  const existingNodeIndex = n.findIndex((node) => node.animationKey === newNode.animationKey);
150
150
  if (existingNodeIndex >= 0) {
151
- const copy = [...n];
152
- copy.splice(existingNodeIndex, 1, newNode);
153
- return copy;
151
+ if (animation) {
152
+ return n.map((n2) =>
153
+ n2.animationKey === newNode.animationKey
154
+ ? { ...newNode, status: 'in' }
155
+ : { ...n2, status: 'out' }
156
+ );
157
+ } else {
158
+ return [{ ...newNode, status: 'in' }];
159
+ }
154
160
  }
155
161
  return [...n, newNode];
156
162
  });
@@ -125,7 +125,14 @@ const ImageWrapper = React.forwardRef(
125
125
  // eslint-disable-next-line react/no-unknown-property
126
126
  fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')}
127
127
  onLoad={(event) => {
128
- events?.onLoad?.forEach((e) => e?.(event));
128
+ if (typeof window !== 'undefined') {
129
+ // this ensures the animation will run, since the starting class is applied at least 1 frame before the target class set in the onLoad event callback
130
+ window.requestAnimationFrame(() => {
131
+ events?.onLoad?.forEach((e) => e?.(event));
132
+ });
133
+ } else {
134
+ events?.onLoad?.forEach((e) => e?.(event));
135
+ }
129
136
  }}
130
137
  onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())}
131
138
  onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))}