* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
679 lines
27 KiB
Diff
679 lines
27 KiB
Diff
diff --git a/node_modules/expo-image/android/build.gradle b/node_modules/expo-image/android/build.gradle
|
|
index 11492a2..b6bb941 100644
|
|
--- a/node_modules/expo-image/android/build.gradle
|
|
+++ b/node_modules/expo-image/android/build.gradle
|
|
@@ -44,7 +44,7 @@ dependencies {
|
|
kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}"
|
|
api 'com.caverock:androidsvg-aar:1.4'
|
|
|
|
- implementation "com.github.penfeizhou.android.animation:glide-plugin:3.0.2"
|
|
+ implementation("com.github.penfeizhou.android.animation:glide-plugin:3.0.3")
|
|
implementation "com.github.bumptech.glide:avif-integration:${GLIDE_VERSION}"
|
|
|
|
api 'com.github.bumptech.glide:okhttp3-integration:4.11.0'
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt
|
|
index 5735515..dbbb924 100644
|
|
--- a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt
|
|
@@ -22,5 +22,19 @@ class ExpoImageAppGlideModule : AppGlideModule() {
|
|
Log.ERROR
|
|
}
|
|
)
|
|
+
|
|
+ // Configure per-server disk cache to match iOS behavior
|
|
+ // Structure: {cacheDir}/{cachePath}/Images/
|
|
+ val maxSizeBytes = 200L * 1024L * 1024L // 200MB per server, matching typical iOS limits
|
|
+ val diskCache = PerServerDiskCache(context.cacheDir, maxSizeBytes)
|
|
+ builder.setDiskCache { diskCache }
|
|
+
|
|
+ // Store reference to access clearPath method
|
|
+ diskCacheInstance = diskCache
|
|
+ }
|
|
+
|
|
+ companion object {
|
|
+ // Store reference to access clearPath method from ExpoImageModule
|
|
+ internal var diskCacheInstance: PerServerDiskCache? = null
|
|
}
|
|
}
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageModule.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageModule.kt
|
|
index 73641a0..186fe54 100644
|
|
--- a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageModule.kt
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageModule.kt
|
|
@@ -148,13 +148,17 @@ class ExpoImageModule : Module() {
|
|
return@AsyncFunction true
|
|
}.runOnQueue(Queues.MAIN)
|
|
|
|
- AsyncFunction<Boolean>("clearDiskCache") {
|
|
- val activity = appContext.currentActivity ?: return@AsyncFunction false
|
|
- activity.let {
|
|
- Glide.get(activity).clearDiskCache()
|
|
+ AsyncFunction("clearDiskCache") Coroutine { cachePath: String? ->
|
|
+ val ctx = appContext.currentActivity ?: appContext.reactContext ?: return@Coroutine false
|
|
+
|
|
+ if (cachePath.isNullOrBlank()) {
|
|
+ Glide.get(ctx).clearDiskCache()
|
|
+ } else {
|
|
+ // Clear only this server's disk cache folder: <cacheDir>/<cachePath>/Images
|
|
+ ExpoImageAppGlideModule.diskCacheInstance?.clearPath(cachePath)
|
|
}
|
|
|
|
- return@AsyncFunction true
|
|
+ true
|
|
}
|
|
|
|
AsyncFunction("getCachePathAsync") { cacheKey: String ->
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/PerServerDiskCache.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/PerServerDiskCache.kt
|
|
new file mode 100644
|
|
index 0000000..7ccf912
|
|
--- /dev/null
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/PerServerDiskCache.kt
|
|
@@ -0,0 +1,96 @@
|
|
+package expo.modules.image
|
|
+
|
|
+import com.bumptech.glide.load.Key
|
|
+import com.bumptech.glide.load.engine.cache.DiskCache
|
|
+import com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper
|
|
+import java.io.File
|
|
+
|
|
+/**
|
|
+ * A disk cache implementation that stores images in separate directories based on cachePath.
|
|
+ * Structure: {rootDir}/{cachePath}/Images/
|
|
+ * This matches the iOS implementation: Library/Caches/{cachePath}/Images/
|
|
+ */
|
|
+class PerServerDiskCache(
|
|
+ private val rootDir: File,
|
|
+ private val maxSizeBytes: Long
|
|
+) : DiskCache {
|
|
+
|
|
+ // Cache the DiskCache instances per cachePath
|
|
+ private val cacheInstances = mutableMapOf<String, DiskCache>()
|
|
+ private val lock = Any()
|
|
+
|
|
+ private fun extractServerKey(key: Key): ServerImageKey? {
|
|
+ return when (key) {
|
|
+ is ServerImageKey -> key
|
|
+ else -> {
|
|
+ try {
|
|
+ val signatureField = key.javaClass.getDeclaredField("signature")
|
|
+ signatureField.isAccessible = true
|
|
+ val inner = signatureField.get(key) as? Key
|
|
+ inner as? ServerImageKey
|
|
+ } catch (e: Exception) {
|
|
+ null
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private fun getCachePathFromKey(key: Key): String {
|
|
+ val serverKey = extractServerKey(key)
|
|
+ return serverKey?.getCachePath() ?: "default"
|
|
+ }
|
|
+
|
|
+ private fun getCacheForPath(cachePath: String): DiskCache {
|
|
+ return synchronized(lock) {
|
|
+ cacheInstances.getOrPut(cachePath) {
|
|
+ val dir = File(File(rootDir, cachePath), "Images")
|
|
+ dir.mkdirs()
|
|
+ DiskLruCacheWrapper.create(dir, maxSizeBytes)
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ override fun get(key: Key): File? {
|
|
+ val cachePath = getCachePathFromKey(key)
|
|
+ val cache = getCacheForPath(cachePath)
|
|
+ return cache.get(key)
|
|
+ }
|
|
+
|
|
+ override fun put(key: Key, writer: DiskCache.Writer) {
|
|
+ val cachePath = getCachePathFromKey(key)
|
|
+ val cache = getCacheForPath(cachePath)
|
|
+ cache.put(key, writer)
|
|
+ }
|
|
+
|
|
+ override fun delete(key: Key) {
|
|
+ val cachePath = getCachePathFromKey(key)
|
|
+ val cache = getCacheForPath(cachePath)
|
|
+ cache.delete(key)
|
|
+ }
|
|
+
|
|
+ override fun clear() {
|
|
+ synchronized(lock) {
|
|
+ cacheInstances.values.forEach { it.clear() }
|
|
+ cacheInstances.clear()
|
|
+ }
|
|
+
|
|
+ // Delete all directories under rootDir
|
|
+ rootDir.deleteRecursively()
|
|
+ rootDir.mkdirs()
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Clear cache for a specific cachePath.
|
|
+ * This allows selective clearing of per-server caches.
|
|
+ */
|
|
+ fun clearPath(cachePath: String) {
|
|
+ synchronized(lock) {
|
|
+ cacheInstances.remove(cachePath)?.clear()
|
|
+ }
|
|
+
|
|
+ val cacheDir = File(File(rootDir, cachePath), "Images")
|
|
+ if (cacheDir.exists()) {
|
|
+ cacheDir.deleteRecursively()
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/ServerImageKey.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/ServerImageKey.kt
|
|
new file mode 100644
|
|
index 0000000..3bb655b
|
|
--- /dev/null
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/ServerImageKey.kt
|
|
@@ -0,0 +1,45 @@
|
|
+package expo.modules.image
|
|
+
|
|
+import com.bumptech.glide.load.Key
|
|
+import java.security.MessageDigest
|
|
+
|
|
+/**
|
|
+ * A cache key that incorporates the server/cachePath, image URL/cacheKey, and optionally
|
|
+ * the app version signature for resource URIs.
|
|
+ * This allows Glide's disk cache to logically separate images by server.
|
|
+ */
|
|
+class ServerImageKey(
|
|
+ private val cachePath: String, // Server identifier (e.g., base64-encoded server URL)
|
|
+ private val imageKey: String, // Original cache key or URL
|
|
+ private val appVersionSignature: Key? = null // Optional app version for resources
|
|
+) : Key {
|
|
+
|
|
+ override fun equals(other: Any?): Boolean {
|
|
+ if (this === other) return true
|
|
+ if (other !is ServerImageKey) return false
|
|
+ return cachePath == other.cachePath &&
|
|
+ imageKey == other.imageKey &&
|
|
+ appVersionSignature == other.appVersionSignature
|
|
+ }
|
|
+
|
|
+ override fun hashCode(): Int {
|
|
+ var result = cachePath.hashCode()
|
|
+ result = 31 * result + imageKey.hashCode()
|
|
+ result = 31 * result + (appVersionSignature?.hashCode() ?: 0)
|
|
+ return result
|
|
+ }
|
|
+
|
|
+ override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
|
+ // Include cachePath in the disk cache key
|
|
+ messageDigest.update(cachePath.toByteArray(Key.CHARSET))
|
|
+ messageDigest.update(0.toByte()) // separator
|
|
+
|
|
+ // Include app version signature if provided (for resource URIs)
|
|
+ appVersionSignature?.updateDiskCacheKey(messageDigest)
|
|
+
|
|
+ // Include the image key
|
|
+ messageDigest.update(imageKey.toByteArray(Key.CHARSET))
|
|
+ }
|
|
+
|
|
+ fun getCachePath(): String = cachePath
|
|
+}
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt
|
|
index 071907c..edf10c1 100644
|
|
--- a/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt
|
|
@@ -70,8 +70,15 @@ data class GlideUrlWrapper(val glideUrl: GlideUrl) {
|
|
|
|
@GlideModule
|
|
class ExpoImageOkHttpClientGlideModule : LibraryGlideModule() {
|
|
+ companion object {
|
|
+ var okHttpClient: OkHttpClient? = null
|
|
+ }
|
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
- val client = OkHttpClient()
|
|
+ val client = if (okHttpClient != null) {
|
|
+ okHttpClient!!
|
|
+ } else {
|
|
+ OkHttpClient()
|
|
+ }
|
|
// We don't use the `GlideUrl` directly but we want to replace the default okhttp loader anyway
|
|
// to make sure that the app will use only one client.
|
|
registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(client))
|
|
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/records/SourceMap.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/records/SourceMap.kt
|
|
index 72ad8fa..60ceaac 100644
|
|
--- a/node_modules/expo-image/android/src/main/java/expo/modules/image/records/SourceMap.kt
|
|
+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/records/SourceMap.kt
|
|
@@ -64,7 +64,8 @@ data class SourceMap(
|
|
@Field override val height: Int = 0,
|
|
@Field override val scale: Double = 1.0,
|
|
@Field val headers: Map<String, String>? = null,
|
|
- @Field val cacheKey: String? = null
|
|
+ @Field val cacheKey: String? = null,
|
|
+ @Field val cachePath: String? = null
|
|
) : Source, Record {
|
|
private var parsedUri: Uri? = null
|
|
|
|
@@ -142,18 +143,41 @@ data class SourceMap(
|
|
|
|
override fun createGlideOptions(context: Context): RequestOptions {
|
|
parseUri(context)
|
|
- return RequestOptions().customize(`when` = width != 0 && height != 0) {
|
|
- // Override the size for local assets (apart from SVGs). This ensures that
|
|
- // resizeMode "center" displays the image in the correct size.
|
|
- override((width * scale).toInt(), (height * scale).toInt())
|
|
- }.customize(`when` = isResourceUri()) {
|
|
+
|
|
+ var options = RequestOptions()
|
|
+ .customize(`when` = width != 0 && height != 0) {
|
|
+ // Override the size for local assets (apart from SVGs). This ensures that
|
|
+ // resizeMode "center" displays the image in the correct size.
|
|
+ override((width * scale).toInt(), (height * scale).toInt())
|
|
+ }
|
|
+
|
|
+ // Build a per-server key if we have a cachePath
|
|
+ val hasServerPath = !cachePath.isNullOrBlank()
|
|
+ val imageKey = cacheKey ?: uri ?: ""
|
|
+
|
|
+ if (hasServerPath) {
|
|
+ val appVersionSig =
|
|
+ if (isResourceUri()) ApplicationVersionSignature.obtain(context) else null
|
|
+
|
|
+ val serverKey = expo.modules.image.ServerImageKey(
|
|
+ cachePath = cachePath!!,
|
|
+ imageKey = imageKey,
|
|
+ appVersionSignature = appVersionSig
|
|
+ )
|
|
+
|
|
+ options = options.signature(serverKey)
|
|
+ } else if (isResourceUri()) {
|
|
// Every local resource (drawable) in Android has its own unique numeric id, which are
|
|
// generated at build time. Although these ids are unique, they are not guaranteed unique
|
|
// across builds. The underlying glide implementation caches these resources. To make
|
|
// sure the cache does not return the wrong image, we should clear the cache when the
|
|
// application version changes.
|
|
- apply(RequestOptions.signatureOf(ApplicationVersionSignature.obtain(context)))
|
|
+ options = options.apply(
|
|
+ RequestOptions.signatureOf(ApplicationVersionSignature.obtain(context))
|
|
+ )
|
|
}
|
|
+
|
|
+ return options
|
|
}
|
|
|
|
private fun getCustomHeaders(): Headers {
|
|
diff --git a/node_modules/expo-image/build/Image.d.ts b/node_modules/expo-image/build/Image.d.ts
|
|
index 44ea175..14b20d9 100644
|
|
--- a/node_modules/expo-image/build/Image.d.ts
|
|
+++ b/node_modules/expo-image/build/Image.d.ts
|
|
@@ -33,6 +33,16 @@ export declare class Image extends React.PureComponent<ImageProps> {
|
|
* finished prefetching.
|
|
*/
|
|
static prefetch(urls: string | string[], options?: ImagePrefetchOptions): Promise<boolean>;
|
|
+ /**
|
|
+ * Preloads images with detailed source information including cache keys.
|
|
+ * @param sources - An array of ImageSource objects with uri, cacheKey, cachePath, etc.
|
|
+ * @param options - Options for prefetching images.
|
|
+ * @return A promise resolving to `true` as soon as all images have been
|
|
+ * successfully prefetched. If an image fails to be prefetched, the promise
|
|
+ * will immediately resolve to `false` regardless of whether other images have
|
|
+ * finished prefetching.
|
|
+ */
|
|
+ static prefetch(sources: ImageSource[], options?: ImagePrefetchOptions): Promise<boolean>;
|
|
/**
|
|
* Asynchronously clears all images stored in memory.
|
|
* @platform android
|
|
@@ -46,11 +56,13 @@ export declare class Image extends React.PureComponent<ImageProps> {
|
|
* Asynchronously clears all images from the disk cache.
|
|
* @platform android
|
|
* @platform ios
|
|
+ * @param path Optional cache path to clear. If provided, only that specific cache is cleared.
|
|
+ * If not provided, clears all caches.
|
|
* @return A promise resolving to `true` when the operation succeeds.
|
|
* It may resolve to `false` on Android when the activity is no longer available.
|
|
* Resolves to `false` on Web.
|
|
*/
|
|
- static clearDiskCache(): Promise<boolean>;
|
|
+ static clearDiskCache(path?: string): Promise<boolean>;
|
|
/**
|
|
* Asynchronously checks if an image exists in the disk cache and resolves to
|
|
* the path of the cached image if it does.
|
|
diff --git a/node_modules/expo-image/build/Image.types.d.ts b/node_modules/expo-image/build/Image.types.d.ts
|
|
index 493fd68..1f71756 100644
|
|
--- a/node_modules/expo-image/build/Image.types.d.ts
|
|
+++ b/node_modules/expo-image/build/Image.types.d.ts
|
|
@@ -46,6 +46,14 @@ export type ImageSource = {
|
|
* If not provided, the `uri` is used also as the cache key.
|
|
*/
|
|
cacheKey?: string;
|
|
+ /**
|
|
+ * Custom cache subdirectory name for this image.
|
|
+ * Will be used as: Library/Caches/ImageCache/{cachePath}/
|
|
+ * If not provided, uses default cache location (SDImageCache.shared).
|
|
+ * @platform ios
|
|
+ * @example "server-abc123" or "external"
|
|
+ */
|
|
+ cachePath?: string;
|
|
/**
|
|
* The max width of the viewport for which this source should be selected.
|
|
* Has no effect if `source` prop is not an array or has only 1 element.
|
|
diff --git a/node_modules/expo-image/ios/ExpoImage.podspec b/node_modules/expo-image/ios/ExpoImage.podspec
|
|
index 21bef71..d662d23 100644
|
|
--- a/node_modules/expo-image/ios/ExpoImage.podspec
|
|
+++ b/node_modules/expo-image/ios/ExpoImage.podspec
|
|
@@ -20,9 +20,7 @@ Pod::Spec.new do |s|
|
|
|
|
s.dependency 'ExpoModulesCore'
|
|
s.dependency 'SDWebImage', '~> 5.19.1'
|
|
- s.dependency 'SDWebImageAVIFCoder', '~> 0.11.0'
|
|
s.dependency 'SDWebImageSVGCoder', '~> 1.7.0'
|
|
- s.dependency 'libavif/libdav1d'
|
|
|
|
# Swift/Objective-C compatibility
|
|
s.pod_target_xcconfig = {
|
|
diff --git a/node_modules/expo-image/ios/ImageLoader.swift b/node_modules/expo-image/ios/ImageLoader.swift
|
|
index a0dacf9..36a2908 100644
|
|
--- a/node_modules/expo-image/ios/ImageLoader.swift
|
|
+++ b/node_modules/expo-image/ios/ImageLoader.swift
|
|
@@ -14,7 +14,10 @@ internal final class ImageLoader {
|
|
func load(_ source: ImageSource, maxSize: CGSize? = nil) async throws -> UIImage {
|
|
// This loader uses only the disk cache. We may want to give more control on this, but the memory cache
|
|
// doesn't make much sense for shared refs as they're kept in memory as long as their JS objects.
|
|
- var context = createSDWebImageContext(forSource: source, cachePolicy: .disk)
|
|
+
|
|
+ // Get custom cache if cachePath is specified
|
|
+ let customCache = CachePathManager.shared.getCacheForPath(source.cachePath)
|
|
+ var context = createSDWebImageContext(forSource: source, cachePolicy: .disk, customCache: customCache)
|
|
|
|
if let maxSize {
|
|
context[.imageThumbnailPixelSize] = maxSize
|
|
diff --git a/node_modules/expo-image/ios/ImageModule.swift b/node_modules/expo-image/ios/ImageModule.swift
|
|
index 4bab386..4d9ca4f 100644
|
|
--- a/node_modules/expo-image/ios/ImageModule.swift
|
|
+++ b/node_modules/expo-image/ios/ImageModule.swift
|
|
@@ -2,7 +2,6 @@
|
|
|
|
import ExpoModulesCore
|
|
import SDWebImage
|
|
-import SDWebImageAVIFCoder
|
|
import SDWebImageSVGCoder
|
|
|
|
public final class ImageModule: Module {
|
|
@@ -150,6 +149,36 @@ public final class ImageModule: Module {
|
|
}
|
|
}
|
|
|
|
+ AsyncFunction("prefetchWithSources") { (sources: [ImageSource], cachePolicy: ImageCachePolicy, promise: Promise) in
|
|
+ var imagesLoaded = 0
|
|
+ var failed = false
|
|
+
|
|
+ sources.forEach { source in
|
|
+ var context = createSDWebImageContext(forSource: source, cachePolicy: cachePolicy)
|
|
+
|
|
+ // Override cache if cachePath is provided
|
|
+ let customCache = CachePathManager.shared.getCacheForPath(source.cachePath)
|
|
+ context[.imageCache] = customCache
|
|
+ context[.originalImageCache] = customCache
|
|
+
|
|
+ guard let url = source.uri else {
|
|
+ return
|
|
+ }
|
|
+
|
|
+ SDWebImagePrefetcher.shared.prefetchURLs([url], context: context, progress: nil, completed: { _, skipped in
|
|
+ if skipped > 0 && !failed {
|
|
+ failed = true
|
|
+ promise.resolve(false)
|
|
+ } else {
|
|
+ imagesLoaded = imagesLoaded + 1
|
|
+ if imagesLoaded == sources.count {
|
|
+ promise.resolve(true)
|
|
+ }
|
|
+ }
|
|
+ })
|
|
+ }
|
|
+ }
|
|
+
|
|
AsyncFunction("generateBlurhashAsync") { (url: URL, numberOfComponents: CGSize, promise: Promise) in
|
|
let downloader = SDWebImageDownloader()
|
|
let parsedNumberOfComponents = (Int(numberOfComponents.width), Int(numberOfComponents.height))
|
|
@@ -170,9 +199,22 @@ public final class ImageModule: Module {
|
|
return true
|
|
}
|
|
|
|
- AsyncFunction("clearDiskCache") { (promise: Promise) in
|
|
- SDImageCache.shared.clearDisk {
|
|
- promise.resolve(true)
|
|
+ AsyncFunction("clearDiskCache") { (cachePath: String?, promise: Promise) in
|
|
+ if let cachePath = cachePath, !cachePath.isEmpty {
|
|
+ // Clear specific cache path
|
|
+ if let cache = CachePathManager.shared.getCacheIfExists(cachePath) {
|
|
+ cache.clearDisk {
|
|
+ promise.resolve(true)
|
|
+ }
|
|
+ } else {
|
|
+ // Cache doesn't exist yet, nothing to clear
|
|
+ promise.resolve(true)
|
|
+ }
|
|
+ } else {
|
|
+ // Clear default shared cache
|
|
+ SDImageCache.shared.clearDisk {
|
|
+ promise.resolve(true)
|
|
+ }
|
|
}
|
|
}
|
|
|
|
@@ -211,7 +253,6 @@ public final class ImageModule: Module {
|
|
static func registerCoders() {
|
|
// By default Animated WebP is not supported
|
|
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
|
|
- SDImageCodersManager.shared.addCoder(SDImageAVIFCoder.shared)
|
|
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
|
|
SDImageCodersManager.shared.addCoder(SDImageHEICCoder.shared)
|
|
}
|
|
diff --git a/node_modules/expo-image/ios/ImageSource.swift b/node_modules/expo-image/ios/ImageSource.swift
|
|
index 88082fd..5025f0e 100644
|
|
--- a/node_modules/expo-image/ios/ImageSource.swift
|
|
+++ b/node_modules/expo-image/ios/ImageSource.swift
|
|
@@ -21,6 +21,9 @@ struct ImageSource: Record {
|
|
@Field
|
|
var cacheKey: String?
|
|
|
|
+ @Field
|
|
+ var cachePath: String?
|
|
+
|
|
var pixelCount: Double {
|
|
return width * height * scale * scale
|
|
}
|
|
diff --git a/node_modules/expo-image/ios/ImageUtils.swift b/node_modules/expo-image/ios/ImageUtils.swift
|
|
index 34f2231..a58943e 100644
|
|
--- a/node_modules/expo-image/ios/ImageUtils.swift
|
|
+++ b/node_modules/expo-image/ios/ImageUtils.swift
|
|
@@ -172,7 +172,7 @@ func createCacheKeyFilter(_ cacheKey: String?) -> SDWebImageCacheKeyFilter? {
|
|
/**
|
|
Creates a default image context based on the source and the cache policy.
|
|
*/
|
|
-func createSDWebImageContext(forSource source: ImageSource, cachePolicy: ImageCachePolicy = .disk) -> SDWebImageContext {
|
|
+func createSDWebImageContext(forSource source: ImageSource, cachePolicy: ImageCachePolicy = .disk, customCache: SDImageCache? = nil) -> SDWebImageContext {
|
|
var context = SDWebImageContext()
|
|
|
|
// Modify URL request to add headers.
|
|
@@ -209,6 +209,12 @@ func createSDWebImageContext(forSource source: ImageSource, cachePolicy: ImageCa
|
|
// Some loaders (e.g. blurhash) may need access to the source.
|
|
context[ImageView.contextSourceKey] = source
|
|
|
|
+ // Override cache instances if custom cache is provided
|
|
+ if let customCache = customCache {
|
|
+ context[.imageCache] = customCache
|
|
+ context[.originalImageCache] = customCache
|
|
+ }
|
|
+
|
|
return context
|
|
}
|
|
|
|
diff --git a/node_modules/expo-image/ios/ImageView.swift b/node_modules/expo-image/ios/ImageView.swift
|
|
index 2ab4cba..0d423ea 100644
|
|
--- a/node_modules/expo-image/ios/ImageView.swift
|
|
+++ b/node_modules/expo-image/ios/ImageView.swift
|
|
@@ -8,6 +8,79 @@ import VisionKit
|
|
|
|
typealias SDWebImageContext = [SDWebImageContextOption: Any]
|
|
|
|
+// MARK: - Cache Path Management
|
|
+
|
|
+internal class CachePathManager {
|
|
+ static let shared = CachePathManager()
|
|
+
|
|
+ // Cache instances by path
|
|
+ private var caches: [String: SDImageCache] = [:]
|
|
+ private let lock = NSLock()
|
|
+
|
|
+ private init() {}
|
|
+
|
|
+ func getCacheForPath(_ cachePath: String?) -> SDImageCache {
|
|
+ // No cachePath provided -> use default singleton
|
|
+ guard let cachePath = cachePath, !cachePath.isEmpty else {
|
|
+ return SDImageCache.shared
|
|
+ }
|
|
+
|
|
+ lock.lock()
|
|
+ defer { lock.unlock() }
|
|
+
|
|
+ // Return existing cache for this path
|
|
+ if let existing = caches[cachePath] {
|
|
+ return existing
|
|
+ }
|
|
+
|
|
+ // Create base ImageCache directory
|
|
+ guard let cachesDir = FileManager.default.urls(
|
|
+ for: .cachesDirectory,
|
|
+ in: .userDomainMask
|
|
+ ).first else {
|
|
+ return SDImageCache.shared
|
|
+ }
|
|
+
|
|
+ // Build path: Library/Caches/{cachePath}/Images/
|
|
+ let imageCacheDir = cachesDir
|
|
+ .appendingPathComponent(cachePath)
|
|
+ .appendingPathComponent("Images")
|
|
+
|
|
+ // Create new cache instance for this path
|
|
+ let cache = SDImageCache(
|
|
+ namespace: "custom",
|
|
+ diskCacheDirectory: imageCacheDir.path
|
|
+ )
|
|
+ caches[cachePath] = cache
|
|
+
|
|
+ return cache
|
|
+ }
|
|
+
|
|
+ func getCacheIfExists(_ cachePath: String) -> SDImageCache? {
|
|
+ lock.lock()
|
|
+ defer { lock.unlock() }
|
|
+ return caches[cachePath]
|
|
+ }
|
|
+
|
|
+ func clearAllCaches(completion: @escaping () -> Void) {
|
|
+ lock.lock()
|
|
+ let allCaches = Array(caches.values)
|
|
+ lock.unlock()
|
|
+
|
|
+ let group = DispatchGroup()
|
|
+ for cache in allCaches {
|
|
+ group.enter()
|
|
+ cache.clearDisk {
|
|
+ group.leave()
|
|
+ }
|
|
+ }
|
|
+
|
|
+ group.notify(queue: .main) {
|
|
+ completion()
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
// swiftlint:disable:next type_body_length
|
|
public final class ImageView: ExpoView {
|
|
static let contextSourceKey = SDWebImageContextOption(rawValue: "source")
|
|
@@ -129,7 +202,10 @@ public final class ImageView: ExpoView {
|
|
if sdImageView.image == nil {
|
|
sdImageView.contentMode = contentFit.toContentMode()
|
|
}
|
|
- var context = createSDWebImageContext(forSource: source, cachePolicy: cachePolicy)
|
|
+
|
|
+ // Get custom cache if cachePath is specified
|
|
+ let customCache = CachePathManager.shared.getCacheForPath(source.cachePath)
|
|
+ var context = createSDWebImageContext(forSource: source, cachePolicy: cachePolicy, customCache: customCache)
|
|
|
|
// Cancel currently running load requests.
|
|
cancelPendingOperation()
|
|
@@ -311,7 +387,8 @@ public final class ImageView: ExpoView {
|
|
// to cache them or apply the same policy as with the proper image?
|
|
// Basically they are also cached in memory as the `placeholderImage` property,
|
|
// so just `disk` policy sounds like a good idea.
|
|
- var context = createSDWebImageContext(forSource: placeholder, cachePolicy: .disk)
|
|
+ let customCache = CachePathManager.shared.getCacheForPath(placeholder.cachePath)
|
|
+ var context = createSDWebImageContext(forSource: placeholder, cachePolicy: .disk, customCache: customCache)
|
|
|
|
let isPlaceholderHash = placeholder.isBlurhash || placeholder.isThumbhash
|
|
|
|
diff --git a/node_modules/expo-image/src/Image.tsx b/node_modules/expo-image/src/Image.tsx
|
|
index 7721bd9..584cb02 100644
|
|
--- a/node_modules/expo-image/src/Image.tsx
|
|
+++ b/node_modules/expo-image/src/Image.tsx
|
|
@@ -69,8 +69,18 @@ export class Image extends React.PureComponent<ImageProps> {
|
|
* finished prefetching.
|
|
*/
|
|
static async prefetch(urls: string | string[], options?: ImagePrefetchOptions): Promise<boolean>;
|
|
+ /**
|
|
+ * Preloads images with detailed source information including cache keys.
|
|
+ * @param sources - An array of ImageSource objects with uri, cacheKey, cachePath, etc.
|
|
+ * @param options - Options for prefetching images.
|
|
+ * @return A promise resolving to `true` as soon as all images have been
|
|
+ * successfully prefetched. If an image fails to be prefetched, the promise
|
|
+ * will immediately resolve to `false` regardless of whether other images have
|
|
+ * finished prefetching.
|
|
+ */
|
|
+ static async prefetch(sources: ImageSource[], options?: ImagePrefetchOptions): Promise<boolean>;
|
|
static async prefetch(
|
|
- urls: string | string[],
|
|
+ urlsOrSources: string | string[] | ImageSource[],
|
|
options?: ImagePrefetchOptions['cachePolicy'] | ImagePrefetchOptions
|
|
): Promise<boolean> {
|
|
let cachePolicy: ImagePrefetchOptions['cachePolicy'] = 'memory-disk';
|
|
@@ -85,7 +95,18 @@ export class Image extends React.PureComponent<ImageProps> {
|
|
break;
|
|
}
|
|
|
|
- return ImageModule.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy, headers);
|
|
+ // Normalize input to array
|
|
+ const sources = Array.isArray(urlsOrSources) ? urlsOrSources : [urlsOrSources];
|
|
+
|
|
+ // Check if we have ImageSource objects or plain strings
|
|
+ const firstItem = sources[0];
|
|
+ if (typeof firstItem === 'string') {
|
|
+ // plain URL strings
|
|
+ return ImageModule.prefetch(sources as string[], cachePolicy, headers);
|
|
+ } else {
|
|
+ // ImageSource objects with cacheKey and cachePath per image
|
|
+ return ImageModule.prefetchWithSources(sources as ImageSource[], cachePolicy);
|
|
+ }
|
|
}
|
|
|
|
/**
|
|
diff --git a/node_modules/expo-image/src/Image.types.ts b/node_modules/expo-image/src/Image.types.ts
|
|
index 63445be..c67ad0c 100644
|
|
--- a/node_modules/expo-image/src/Image.types.ts
|
|
+++ b/node_modules/expo-image/src/Image.types.ts
|
|
@@ -51,6 +51,16 @@ export type ImageSource = {
|
|
* If not provided, the `uri` is used also as the cache key.
|
|
*/
|
|
cacheKey?: string;
|
|
+ /**
|
|
+ * Custom cache subdirectory name for this image.
|
|
+ * - iOS: Library/Caches/{cachePath}/Images/
|
|
+ * - Android: {cacheDir}/{cachePath}/Images/
|
|
+ * If not provided, uses default cache location.
|
|
+ * @platform ios
|
|
+ * @platform android
|
|
+ * @example "server-abc123" or "external"
|
|
+ */
|
|
+ cachePath?: string;
|
|
/**
|
|
* The max width of the viewport for which this source should be selected.
|
|
* Has no effect if `source` prop is not an array or has only 1 element.
|