@shortkitsdk/react-native 0.2.12 → 0.2.15

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.
Files changed (65) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +47 -4
  2. package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +117 -2
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +7 -5
  5. package/ios/ReactCarouselOverlayHost.swift +67 -35
  6. package/ios/ReactOverlayHost.swift +85 -75
  7. package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
  8. package/ios/ShortKitBridge.swift +122 -20
  9. package/ios/ShortKitModule.mm +15 -5
  10. package/ios/ShortKitSDK.xcframework/Info.plist +5 -4
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -0
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +5 -1
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2488 -281
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +65 -5
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +65 -5
  17. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  18. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  19. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +2 -1
  20. package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Info.plist +5 -1
  21. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +8233 -3592
  22. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +120 -19
  23. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +120 -19
  25. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +33558 -0
  26. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +925 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  28. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +925 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  30. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +212 -0
  31. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  32. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +7338 -3272
  33. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +106 -15
  34. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  35. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +106 -15
  36. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  37. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  38. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1838 -206
  39. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +51 -1
  40. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  41. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +51 -1
  42. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  43. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  44. package/package.json +1 -1
  45. package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
  46. package/src/ShortKitContext.ts +11 -0
  47. package/src/ShortKitFeed.tsx +25 -10
  48. package/src/ShortKitOverlaySurface.tsx +24 -11
  49. package/src/ShortKitProvider.tsx +4 -2
  50. package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
  51. package/src/ShortKitWidget.tsx +3 -3
  52. package/src/index.ts +5 -1
  53. package/src/serialization.ts +7 -0
  54. package/src/specs/NativeShortKitModule.ts +18 -2
  55. package/src/types.ts +48 -3
  56. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  57. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  58. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  59. package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  60. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/Info.plist +4 -4
  61. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Info.plist +0 -0
  62. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  63. /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -0
  64. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +0 -0
  65. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +0 -0
@@ -17,6 +17,7 @@ import com.shortkit.sdk.model.FeedInput
17
17
  import com.shortkit.sdk.model.FeedScrollPhase
18
18
  import com.shortkit.sdk.model.FeedTransitionPhase
19
19
  import com.shortkit.sdk.model.ImageCarouselItem
20
+ import com.shortkit.sdk.model.VideoCarouselItem
20
21
  import com.shortkit.sdk.model.JsonValue
21
22
  import com.shortkit.sdk.model.PlayerState
22
23
  import com.shortkit.sdk.config.AdOverlayMode
@@ -26,6 +27,8 @@ import com.shortkit.sdk.config.FeedHeight
26
27
  import com.shortkit.sdk.config.FeedSource
27
28
  import com.shortkit.sdk.config.ScrollAxis
28
29
  import com.shortkit.sdk.config.SurveyOverlayMode
30
+ import com.shortkit.sdk.config.VideoCarouselOverlayMode
31
+ import com.shortkit.sdk.feed.ShortKitRefreshState
29
32
  import com.shortkit.sdk.config.VideoOverlayMode
30
33
  import com.shortkit.sdk.feed.FeedPreload
31
34
  import com.shortkit.sdk.feed.ShortKitFeedFragment
@@ -152,16 +155,21 @@ class ShortKitBridge(
152
155
 
153
156
  val carouselOverlayRaw = obj.optString("carouselOverlay", null)
154
157
  val carouselOverlay = parseCarouselOverlay(carouselOverlayRaw, context)
158
+ val videoCarouselOverlayRaw = obj.optString("videoCarouselOverlay", null)
159
+ val videoCarouselOverlay = parseVideoCarouselOverlay(videoCarouselOverlayRaw, context)
155
160
  val autoplay = obj.optBoolean("autoplay", true)
156
161
  val filter = obj.optJSONObject("filter")?.let { parseFeedFilterToModel(it.toString()) }
157
162
 
158
163
  val scrollAxisStr = obj.optString("scrollAxis", "vertical")
159
164
  val scrollAxis = if (scrollAxisStr == "horizontal") ScrollAxis.Horizontal else ScrollAxis.Vertical
160
165
 
166
+ val pullToRefreshEnabled = obj.optBoolean("pullToRefreshEnabled", true)
167
+
161
168
  FeedConfig(
162
169
  feedHeight = feedHeight,
163
170
  videoOverlay = videoOverlay,
164
171
  carouselOverlay = carouselOverlay,
172
+ videoCarouselOverlay = videoCarouselOverlay,
165
173
  surveyOverlay = SurveyOverlayMode.None,
166
174
  adOverlay = AdOverlayMode.None,
167
175
  muteOnStart = muteOnStart,
@@ -169,6 +177,7 @@ class ShortKitBridge(
169
177
  feedSource = feedSource,
170
178
  filter = filter,
171
179
  scrollAxis = scrollAxis,
180
+ pullToRefreshEnabled = pullToRefreshEnabled,
172
181
  )
173
182
  } catch (e: Exception) {
174
183
  android.util.Log.e("SK:Bridge", "parseFeedConfig: EXCEPTION parsing config, returning default. json=${json.take(300)}", e)
@@ -273,6 +282,43 @@ class ShortKitBridge(
273
282
  }
274
283
  }
275
284
 
285
+ /**
286
+ * Parse a double-stringified video carousel overlay JSON.
287
+ * - `"\"none\""` -> None
288
+ * - `"{\"type\":\"custom\"}"` -> Custom with ReactVideoCarouselOverlayHost factory
289
+ */
290
+ private fun parseVideoCarouselOverlay(json: String?, context: android.content.Context?): VideoCarouselOverlayMode {
291
+ if (json.isNullOrEmpty()) return VideoCarouselOverlayMode.None
292
+ return try {
293
+ val parsed = json.trim()
294
+
295
+ if (parsed == "\"none\"" || parsed == "none") {
296
+ return VideoCarouselOverlayMode.None
297
+ }
298
+
299
+ val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
300
+ JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
301
+ } else {
302
+ JSONObject(parsed)
303
+ }
304
+
305
+ if (inner.optString("type") == "custom" && context != null) {
306
+ val name = inner.optString("name", "Default")
307
+ val ctx = context.applicationContext
308
+ VideoCarouselOverlayMode.Custom {
309
+ ReactVideoCarouselOverlayHost(ctx).apply {
310
+ videoCarouselOverlayName = name
311
+ prepareSurface()
312
+ }
313
+ }
314
+ } else {
315
+ VideoCarouselOverlayMode.None
316
+ }
317
+ } catch (_: Exception) {
318
+ VideoCarouselOverlayMode.None
319
+ }
320
+ }
321
+
276
322
  private fun parseFeedFilter(json: String?): String? {
277
323
  // On Android, the SDK accepts the raw JSON string for filtering.
278
324
  // Return as-is if non-null/non-empty.
@@ -316,6 +362,26 @@ class ShortKitBridge(
316
362
  val carouselItem = parseImageCarouselItem(itemObj) ?: continue
317
363
  result.add(FeedInput.ImageCarousel(carouselItem))
318
364
  }
365
+ "videoCarousel" -> {
366
+ val itemObj = obj.optJSONObject("item") ?: continue
367
+ val videosArr = itemObj.optJSONArray("videos") ?: continue
368
+ val videos = mutableListOf<ContentItem>()
369
+ for (i in 0 until videosArr.length()) {
370
+ val videoObj = videosArr.getJSONObject(i)
371
+ parseContentItem(videoObj)?.let { videos.add(it) }
372
+ }
373
+ if (videos.isEmpty()) continue
374
+ val carouselItem = VideoCarouselItem(
375
+ id = itemObj.getString("id"),
376
+ videos = videos,
377
+ title = itemObj.optString("title", null),
378
+ description = itemObj.optString("description", null),
379
+ author = itemObj.optString("author", null),
380
+ section = itemObj.optString("section", null),
381
+ articleUrl = itemObj.optString("articleUrl", null),
382
+ )
383
+ result.add(FeedInput.VideoCarousel(carouselItem))
384
+ }
319
385
  }
320
386
  }
321
387
  result.ifEmpty { null }
@@ -349,6 +415,27 @@ class ShortKitBridge(
349
415
  )
350
416
  }
351
417
 
418
+ private fun parseContentItem(obj: JSONObject): ContentItem? {
419
+ val id = obj.optString("id", null) ?: return null
420
+ val title = obj.optString("title", null) ?: return null
421
+ val duration = obj.optDouble("duration", -1.0).takeIf { it >= 0 } ?: return null
422
+ val streamingUrl = obj.optString("streamingUrl", null) ?: return null
423
+ val thumbnailUrl = obj.optString("thumbnailUrl", null) ?: return null
424
+ return ContentItem(
425
+ id = id,
426
+ playbackId = obj.optString("playbackId", null),
427
+ title = title,
428
+ description = obj.optString("description", null),
429
+ duration = duration,
430
+ streamingUrl = streamingUrl,
431
+ thumbnailUrl = thumbnailUrl,
432
+ captionTracks = emptyList(),
433
+ author = obj.optString("author", null),
434
+ articleUrl = obj.optString("articleUrl", null),
435
+ fallbackUrl = obj.optString("fallbackUrl", null),
436
+ )
437
+ }
438
+
352
439
  private fun buildCaptionTracksJSONArray(tracks: List<CaptionTrack>): JSONArray {
353
440
  val arr = JSONArray()
354
441
  for (track in tracks) {
@@ -405,6 +492,7 @@ class ShortKitBridge(
405
492
  private val feedRegistry = mutableMapOf<String, WeakReference<ShortKitFeedFragment>>()
406
493
  private val pendingOps = mutableMapOf<String, MutableList<(ShortKitFeedFragment) -> Unit>>()
407
494
  private val pendingOpsLock = Any()
495
+ private var lastProgressEmitTime = 0L
408
496
 
409
497
  /** Expose the underlying SDK for the Fabric feed view manager. */
410
498
  val sdk: ShortKit? get() = shortKit
@@ -445,8 +533,35 @@ class ShortKitBridge(
445
533
  emitEventOnMain("onContentTapped", params)
446
534
  }
447
535
 
448
- override fun onRefreshRequested() {
449
- emitEventOnMain("onRefreshRequested", Arguments.createMap())
536
+ override fun onRefreshStateChanged(state: ShortKitRefreshState) {
537
+ // Throttle pulling events to max 1 per 16ms to avoid bridge saturation
538
+ if (state is ShortKitRefreshState.Pulling) {
539
+ val now = android.os.SystemClock.uptimeMillis()
540
+ if (now - lastProgressEmitTime < 16L) return
541
+ lastProgressEmitTime = now
542
+ }
543
+
544
+ val params = Arguments.createMap().apply {
545
+ when (state) {
546
+ is ShortKitRefreshState.Idle -> {
547
+ putString("status", "idle")
548
+ putDouble("progress", 0.0)
549
+ }
550
+ is ShortKitRefreshState.Pulling -> {
551
+ putString("status", "pulling")
552
+ putDouble("progress", state.progress.toDouble())
553
+ }
554
+ is ShortKitRefreshState.Triggered -> {
555
+ putString("status", "triggered")
556
+ putDouble("progress", 0.0)
557
+ }
558
+ is ShortKitRefreshState.Refreshing -> {
559
+ putString("status", "refreshing")
560
+ putDouble("progress", 0.0)
561
+ }
562
+ }
563
+ }
564
+ emitEventOnMain("onRefreshStateChanged", params)
450
565
  }
451
566
 
452
567
  override fun onFeedContentFetched(items: List<ContentItem>) {
@@ -207,11 +207,11 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
207
207
  @ReactMethod
208
208
  override fun preloadFeed(configJSON: String, itemsJSON: String?, promise: Promise) {
209
209
  val b = bridge
210
- if (b == null) {
211
- promise.resolve("")
212
- return
210
+ if (b != null) {
211
+ b.preloadFeed(configJSON, itemsJSON) { result -> promise.resolve(result) }
212
+ } else {
213
+ bufferOp { bridge?.preloadFeed(configJSON, itemsJSON) { result -> promise.resolve(result) } }
213
214
  }
214
- b.preloadFeed(configJSON, itemsJSON) { result -> promise.resolve(result) }
215
215
  }
216
216
 
217
217
  // -----------------------------------------------------------------
@@ -261,7 +261,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
261
261
  "onSurveyResponse" -> emitOnSurveyResponse(params)
262
262
  "onContentTapped" -> emitOnContentTapped(params)
263
263
  "onDismiss" -> emitOnDismiss(params)
264
- "onRefreshRequested" -> emitOnRefreshRequested(params)
264
+ "onRefreshStateChanged" -> emitOnRefreshStateChanged(params)
265
265
  "onDidFetchContentItems" -> emitOnDidFetchContentItems(params)
266
266
  "onFeedReady" -> emitOnFeedReady(params)
267
267
  "onOverlayActiveChanged" -> emitOnOverlayActiveChanged(params)
@@ -273,6 +273,8 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
273
273
  "onOverlayFeedScrollPhaseChanged" -> emitOnOverlayFeedScrollPhaseChanged(params)
274
274
  "onOverlayTimeUpdate" -> emitOnOverlayTimeUpdate(params)
275
275
  "onOverlayFullState" -> emitOnOverlayFullState(params)
276
+ "onCarouselActiveImageChanged" -> emitOnCarouselActiveImageChanged(params)
277
+ "onVideoCarouselActiveVideoChanged" -> emitOnVideoCarouselActiveVideoChanged(params)
276
278
  else -> {
277
279
  android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
278
280
  reactApplicationContext
@@ -5,8 +5,8 @@ import ShortKitSDK
5
5
  /// for rendering the developer's React carousel overlay component inside a feed cell.
6
6
  ///
7
7
  /// Unlike ReactOverlayHost, this does not subscribe to player state (carousels
8
- /// are image-based). It emits image URLs to JS for Image.prefetch() and pushes
9
- /// ImageCarouselItem data as appProperties.
8
+ /// are image-based). It delivers `isActive` and `activeImageIndex` via bridge
9
+ /// events (routed by surfaceId) to avoid Fabric remounts.
10
10
  @objc public class ReactCarouselOverlayHost: UIView, @unchecked Sendable, CarouselOverlay {
11
11
 
12
12
  // MARK: - Configuration
@@ -20,9 +20,6 @@ import ShortKitSDK
20
20
 
21
21
  // MARK: - Eager Surface Creation
22
22
 
23
- /// Eagerly create the RN surface so it's ready before the first configure().
24
- /// Called by the overlay factory right after setting surfacePresenter and moduleName.
25
- /// Mirrors ReactOverlayHost.attach(player:) which does the same for video overlays.
26
23
  func prepareSurface() {
27
24
  createSurfaceIfNeeded()
28
25
  }
@@ -37,6 +34,14 @@ import ShortKitSDK
37
34
  private var surface: SKFabricSurfaceWrapper?
38
35
  private var surfaceView: UIView?
39
36
  private var pendingProps: [String: Any]?
37
+ private var isActive = false
38
+ private var activeImageIndex = 0
39
+
40
+ /// Serial queue for JPEG encoding + temp file writes (off main thread).
41
+ private static let imageWriteQueue = DispatchQueue(label: "com.shortkit.carousel-image-write", qos: .userInitiated)
42
+
43
+ /// Unique identifier for this overlay instance, used for event routing.
44
+ let surfaceId = UUID().uuidString
40
45
 
41
46
  // MARK: - Init
42
47
 
@@ -57,19 +62,32 @@ import ShortKitSDK
57
62
  // MARK: - CarouselOverlay
58
63
 
59
64
  public func configure(with item: ImageCarouselItem) {
65
+ isActive = false
66
+ activeImageIndex = 0
60
67
  createSurfaceIfNeeded()
61
68
 
62
- // Replace remote URLs with local file URLs for any natively-cached images.
63
- // The SDK's PrefetchManager downloads images into NSCache<NSString, UIImage>
64
- // which RN can't access. Writing to temp files lets RN load instantly from disk.
69
+ // Replace remote URLs with local file URLs for any natively-cached images
70
+ // that already have a temp file on disk (fast path: FileManager.fileExists).
71
+ // If the native cache has the image but no temp file exists yet, use the
72
+ // remote URL immediately and schedule a background write so the file://
73
+ // URL is available on the next configure() for this image.
65
74
  var modifiedItem = item
66
75
  if let cachedImage {
67
76
  var localImages: [CarouselImage] = []
68
77
  for image in item.images {
69
- if let uiImage = cachedImage(image.url),
70
- let localURL = writeTempImage(uiImage, for: image.url) {
71
- localImages.append(CarouselImage(url: localURL, alt: image.alt))
78
+ let tempURL = Self.tempFileURL(for: image.url)
79
+ if FileManager.default.fileExists(atPath: tempURL.path) {
80
+ // Fast path: temp file already written — use file:// URL
81
+ localImages.append(CarouselImage(url: tempURL.absoluteString, alt: image.alt))
82
+ } else if let uiImage = cachedImage(image.url) {
83
+ // Native cache hit but no temp file yet — use remote URL now,
84
+ // write temp file in background for next time.
85
+ localImages.append(image)
86
+ Self.imageWriteQueue.async {
87
+ Self.writeTempImage(uiImage, to: tempURL)
88
+ }
72
89
  } else {
90
+ // Not cached — use remote URL
73
91
  localImages.append(image)
74
92
  }
75
93
  }
@@ -85,10 +103,12 @@ import ShortKitSDK
85
103
  )
86
104
  }
87
105
 
88
- // Build props — push now if surface exists, otherwise store for later
89
106
  if let data = try? JSONEncoder().encode(modifiedItem),
90
107
  let json = String(data: data, encoding: .utf8) {
91
- let props: [String: Any] = ["item": json]
108
+ let props: [String: Any] = [
109
+ "surfaceId": surfaceId,
110
+ "item": json,
111
+ ]
92
112
  if let surface {
93
113
  surface.setProperties(props)
94
114
  } else {
@@ -97,32 +117,46 @@ import ShortKitSDK
97
117
  }
98
118
  }
99
119
 
100
- /// Write a UIImage to the temp directory, returning a file:// URL string.
101
- /// Uses a hash of the remote URL as the filename to avoid duplicates.
102
- private func writeTempImage(_ image: UIImage, for remoteURL: String) -> String? {
120
+ public func activatePlayback() {
121
+ isActive = true
122
+ bridge?.emit("onOverlayFullState", body: [
123
+ "surfaceId": surfaceId,
124
+ "isActive": true,
125
+ // Include required fields for the full state event
126
+ "playerState": "idle",
127
+ "isMuted": true,
128
+ "playbackRate": 1.0,
129
+ "captionsEnabled": false,
130
+ "activeCue": NSNull(),
131
+ "feedScrollPhase": NSNull(),
132
+ ])
133
+ }
134
+
135
+ public func updateActiveImage(index: Int) {
136
+ activeImageIndex = index
137
+ bridge?.emit("onCarouselActiveImageChanged", body: [
138
+ "surfaceId": surfaceId,
139
+ "activeImageIndex": index,
140
+ ])
141
+ }
142
+
143
+ /// Deterministic temp file URL for a given remote image URL.
144
+ private static func tempFileURL(for remoteURL: String) -> URL {
103
145
  let hash = remoteURL.hash
104
146
  let fileName = "sk-carousel-\(abs(hash)).jpg"
105
- let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
106
-
107
- // Skip if already written
108
- if FileManager.default.fileExists(atPath: fileURL.path) {
109
- return fileURL.absoluteString
110
- }
147
+ return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
148
+ }
111
149
 
112
- guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
113
- do {
114
- try data.write(to: fileURL, options: .atomic)
115
- return fileURL.absoluteString
116
- } catch {
117
- return nil
118
- }
150
+ /// JPEG-encode a UIImage and write to the given file URL.
151
+ /// Safe to call from any queue — no UIKit main-thread requirements.
152
+ private static func writeTempImage(_ image: UIImage, to fileURL: URL) {
153
+ guard !FileManager.default.fileExists(atPath: fileURL.path),
154
+ let data = image.jpegData(compressionQuality: 0.9) else { return }
155
+ try? data.write(to: fileURL, options: .atomic)
119
156
  }
120
157
 
121
158
  public func resetState() {
122
- // Don't clear surface props — matches video overlay (ReactOverlayHost) pattern.
123
- // configure() will overwrite with new props directly, letting React do an
124
- // in-place re-render (old→new) instead of an unmount+remount (old→empty→new).
125
- // The overlay's synchronous reset pattern handles state transitions.
159
+ // Don't clear surface props — configure() will overwrite.
126
160
  }
127
161
 
128
162
  // MARK: - Surface Creation
@@ -130,7 +164,6 @@ import ShortKitSDK
130
164
  private func createSurfaceIfNeeded() {
131
165
  guard surface == nil, let presenter = surfacePresenter else { return }
132
166
 
133
- // Must always dispatch to main queue — see ReactOverlayHost for details.
134
167
  DispatchQueue.main.async { [weak self] in
135
168
  guard let self, self.surface == nil else { return }
136
169
  self.installSurface(presenter: presenter)
@@ -157,7 +190,6 @@ import ShortKitSDK
157
190
  surfaceView = view
158
191
  surface = surf
159
192
 
160
- // Flush any props that arrived before surface was ready
161
193
  if let pending = pendingProps {
162
194
  surf.setProperties(pending)
163
195
  pendingProps = nil