@shortkitsdk/react-native 0.2.14 → 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 (74) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +34 -2
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +5 -5
  3. package/ios/ReactCarouselOverlayHost.swift +30 -18
  4. package/ios/ReactOverlayHost.swift +65 -67
  5. package/ios/ShortKitBridge.swift +80 -20
  6. package/ios/ShortKitModule.mm +13 -4
  7. package/ios/ShortKitSDK.xcframework/Info.plist +2 -1
  8. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -0
  9. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +5 -1
  10. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +672 -97
  11. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +15 -5
  12. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +15 -5
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +11 -11
  16. package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -0
  17. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Info.plist +5 -1
  18. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2488 -281
  19. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +65 -5
  20. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +65 -5
  22. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json} +8233 -3592
  23. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +925 -0
  24. package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc} +0 -0
  25. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +925 -0
  26. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +212 -0
  28. package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +1838 -206
  29. package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +51 -1
  30. package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  31. package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +51 -1
  32. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  33. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
  34. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +6 -6
  35. package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  36. package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +2 -2
  37. package/package.json +1 -1
  38. package/src/ShortKitContext.ts +11 -0
  39. package/src/ShortKitFeed.tsx +20 -9
  40. package/src/ShortKitOverlaySurface.tsx +20 -6
  41. package/src/ShortKitProvider.tsx +4 -2
  42. package/src/ShortKitWidget.tsx +3 -3
  43. package/src/index.ts +1 -0
  44. package/src/specs/NativeShortKitModule.ts +5 -2
  45. package/src/types.ts +9 -2
  46. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +0 -16
  47. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +0 -4
  48. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -418
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +0 -28917
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +0 -824
  52. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  53. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +0 -824
  54. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  55. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -418
  56. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +0 -824
  57. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +0 -824
  58. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  59. package/ios/ShortKitSDK.xcframework.bak2/Info.plist +0 -43
  60. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Info.plist +0 -16
  61. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +0 -4
  62. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  63. package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  64. package/ios/{ShortKitSDK.xcframework.bak2/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  65. package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/Info.plist +4 -4
  66. /package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -0
  67. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Info.plist +0 -0
  68. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +0 -0
  69. /package/ios/{ShortKitSDK.xcframework.bak2 → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -0
  70. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +0 -0
  71. /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +0 -0
  72. /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  73. /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +0 -0
  74. /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +0 -0
@@ -28,6 +28,7 @@ import com.shortkit.sdk.config.FeedSource
28
28
  import com.shortkit.sdk.config.ScrollAxis
29
29
  import com.shortkit.sdk.config.SurveyOverlayMode
30
30
  import com.shortkit.sdk.config.VideoCarouselOverlayMode
31
+ import com.shortkit.sdk.feed.ShortKitRefreshState
31
32
  import com.shortkit.sdk.config.VideoOverlayMode
32
33
  import com.shortkit.sdk.feed.FeedPreload
33
34
  import com.shortkit.sdk.feed.ShortKitFeedFragment
@@ -162,6 +163,8 @@ class ShortKitBridge(
162
163
  val scrollAxisStr = obj.optString("scrollAxis", "vertical")
163
164
  val scrollAxis = if (scrollAxisStr == "horizontal") ScrollAxis.Horizontal else ScrollAxis.Vertical
164
165
 
166
+ val pullToRefreshEnabled = obj.optBoolean("pullToRefreshEnabled", true)
167
+
165
168
  FeedConfig(
166
169
  feedHeight = feedHeight,
167
170
  videoOverlay = videoOverlay,
@@ -174,6 +177,7 @@ class ShortKitBridge(
174
177
  feedSource = feedSource,
175
178
  filter = filter,
176
179
  scrollAxis = scrollAxis,
180
+ pullToRefreshEnabled = pullToRefreshEnabled,
177
181
  )
178
182
  } catch (e: Exception) {
179
183
  android.util.Log.e("SK:Bridge", "parseFeedConfig: EXCEPTION parsing config, returning default. json=${json.take(300)}", e)
@@ -488,6 +492,7 @@ class ShortKitBridge(
488
492
  private val feedRegistry = mutableMapOf<String, WeakReference<ShortKitFeedFragment>>()
489
493
  private val pendingOps = mutableMapOf<String, MutableList<(ShortKitFeedFragment) -> Unit>>()
490
494
  private val pendingOpsLock = Any()
495
+ private var lastProgressEmitTime = 0L
491
496
 
492
497
  /** Expose the underlying SDK for the Fabric feed view manager. */
493
498
  val sdk: ShortKit? get() = shortKit
@@ -528,8 +533,35 @@ class ShortKitBridge(
528
533
  emitEventOnMain("onContentTapped", params)
529
534
  }
530
535
 
531
- override fun onRefreshRequested() {
532
- 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)
533
565
  }
534
566
 
535
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)
@@ -37,6 +37,9 @@ import ShortKitSDK
37
37
  private var isActive = false
38
38
  private var activeImageIndex = 0
39
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
+
40
43
  /// Unique identifier for this overlay instance, used for event routing.
41
44
  let surfaceId = UUID().uuidString
42
45
 
@@ -63,15 +66,28 @@ import ShortKitSDK
63
66
  activeImageIndex = 0
64
67
  createSurfaceIfNeeded()
65
68
 
66
- // Replace remote URLs with local file URLs for any natively-cached images.
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.
67
74
  var modifiedItem = item
68
75
  if let cachedImage {
69
76
  var localImages: [CarouselImage] = []
70
77
  for image in item.images {
71
- if let uiImage = cachedImage(image.url),
72
- let localURL = writeTempImage(uiImage, for: image.url) {
73
- 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
+ }
74
89
  } else {
90
+ // Not cached — use remote URL
75
91
  localImages.append(image)
76
92
  }
77
93
  }
@@ -124,23 +140,19 @@ import ShortKitSDK
124
140
  ])
125
141
  }
126
142
 
127
- /// Write a UIImage to the temp directory, returning a file:// URL string.
128
- private func writeTempImage(_ image: UIImage, for remoteURL: String) -> String? {
143
+ /// Deterministic temp file URL for a given remote image URL.
144
+ private static func tempFileURL(for remoteURL: String) -> URL {
129
145
  let hash = remoteURL.hash
130
146
  let fileName = "sk-carousel-\(abs(hash)).jpg"
131
- let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
132
-
133
- if FileManager.default.fileExists(atPath: fileURL.path) {
134
- return fileURL.absoluteString
135
- }
147
+ return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
148
+ }
136
149
 
137
- guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
138
- do {
139
- try data.write(to: fileURL, options: .atomic)
140
- return fileURL.absoluteString
141
- } catch {
142
- return nil
143
- }
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)
144
156
  }
145
157
 
146
158
  public func resetState() {
@@ -46,7 +46,9 @@ import ShortKitSDK
46
46
  private var cachedPlaybackRate: Double = 1.0
47
47
  private var cachedCaptionsEnabled: Bool = false
48
48
  private var cachedActiveCue: [String: Any]? = nil
49
- private var cachedFeedScrollPhase: String? = nil
49
+ private var cachedActiveCueJson: String? = nil // pre-serialized for bridge
50
+ private var cachedFeedScrollPhase: String? = nil // already a JSON string
51
+ private var isDragging: Bool = false // true while scroll phase is .dragging
50
52
 
51
53
  // Time coalescing
52
54
  private var cachedTime: (current: Double, duration: Double, buffered: Double) = (0, 0, 0)
@@ -137,7 +139,9 @@ import ShortKitSDK
137
139
  cachedTime = (0, 0, 0)
138
140
  cachedPlayerState = "idle"
139
141
  cachedActiveCue = nil
142
+ cachedActiveCueJson = nil
140
143
  cachedFeedScrollPhase = nil
144
+ isDragging = false
141
145
 
142
146
  createSurfaceIfNeeded()
143
147
 
@@ -165,30 +169,21 @@ import ShortKitSDK
165
169
  }
166
170
  }
167
171
 
168
- /// Emit all cached state as individual events. Called from activatePlayback()
169
- /// (deferred) and can be called whenever we need to synchronize JS state.
172
+ /// Emit all cached state as a single batched event. Called from
173
+ /// activatePlayback() (deferred) and on drag→settled transitions.
174
+ /// Uses pre-serialized JSON strings to avoid JSONSerialization on the
175
+ /// main thread during the swipe-settle critical path.
170
176
  private func emitFullState() {
171
- var body: [String: Any] = [
177
+ bridge?.emit("onOverlayFullState", body: [
172
178
  "surfaceId": surfaceId,
173
179
  "isActive": true,
174
180
  "playerState": cachedPlayerState,
175
181
  "isMuted": cachedIsMuted,
176
182
  "playbackRate": cachedPlaybackRate,
177
183
  "captionsEnabled": cachedCaptionsEnabled,
178
- ]
179
- if let cue = cachedActiveCue,
180
- let cueData = try? JSONSerialization.data(withJSONObject: cue),
181
- let cueJson = String(data: cueData, encoding: .utf8) {
182
- body["activeCue"] = cueJson
183
- } else {
184
- body["activeCue"] = NSNull()
185
- }
186
- if let scrollPhase = cachedFeedScrollPhase {
187
- body["feedScrollPhase"] = scrollPhase
188
- } else {
189
- body["feedScrollPhase"] = NSNull()
190
- }
191
- bridge?.emit("onOverlayFullState", body: body)
184
+ "activeCue": cachedActiveCueJson ?? NSNull(),
185
+ "feedScrollPhase": cachedFeedScrollPhase ?? NSNull(),
186
+ ])
192
187
  }
193
188
 
194
189
  // MARK: - Surface Creation
@@ -255,17 +250,21 @@ import ShortKitSDK
255
250
  // MARK: - Player Subscriptions
256
251
 
257
252
  private func subscribeToPlayer(_ player: ShortKitPlayer) {
253
+ // Player state, muted, playbackRate, captionsEnabled, and activeCue
254
+ // are suppressed during .dragging to avoid unnecessary bridge traffic
255
+ // for an overlay that is scrolling off screen. emitFullState() re-syncs
256
+ // all cached values when the scroll settles (or on cancelled swipe).
257
+
258
258
  player.playerState
259
259
  .receive(on: DispatchQueue.main)
260
260
  .sink { [weak self] state in
261
261
  guard let self else { return }
262
262
  self.cachedPlayerState = Self.playerStateString(state)
263
- if self.isActive {
264
- self.bridge?.emit("onOverlayPlayerStateChanged", body: [
265
- "surfaceId": self.surfaceId,
266
- "playerState": self.cachedPlayerState
267
- ])
268
- }
263
+ guard self.isActive, !self.isDragging else { return }
264
+ self.bridge?.emit("onOverlayPlayerStateChanged", body: [
265
+ "surfaceId": self.surfaceId,
266
+ "playerState": self.cachedPlayerState
267
+ ])
269
268
  }
270
269
  .store(in: &cancellables)
271
270
 
@@ -274,12 +273,11 @@ import ShortKitSDK
274
273
  .sink { [weak self] muted in
275
274
  guard let self else { return }
276
275
  self.cachedIsMuted = muted
277
- if self.isActive {
278
- self.bridge?.emit("onOverlayMutedChanged", body: [
279
- "surfaceId": self.surfaceId,
280
- "isMuted": self.cachedIsMuted
281
- ])
282
- }
276
+ guard self.isActive, !self.isDragging else { return }
277
+ self.bridge?.emit("onOverlayMutedChanged", body: [
278
+ "surfaceId": self.surfaceId,
279
+ "isMuted": self.cachedIsMuted
280
+ ])
283
281
  }
284
282
  .store(in: &cancellables)
285
283
 
@@ -288,12 +286,11 @@ import ShortKitSDK
288
286
  .sink { [weak self] rate in
289
287
  guard let self else { return }
290
288
  self.cachedPlaybackRate = Double(rate)
291
- if self.isActive {
292
- self.bridge?.emit("onOverlayPlaybackRateChanged", body: [
293
- "surfaceId": self.surfaceId,
294
- "playbackRate": self.cachedPlaybackRate
295
- ])
296
- }
289
+ guard self.isActive, !self.isDragging else { return }
290
+ self.bridge?.emit("onOverlayPlaybackRateChanged", body: [
291
+ "surfaceId": self.surfaceId,
292
+ "playbackRate": self.cachedPlaybackRate
293
+ ])
297
294
  }
298
295
  .store(in: &cancellables)
299
296
 
@@ -302,12 +299,11 @@ import ShortKitSDK
302
299
  .sink { [weak self] enabled in
303
300
  guard let self else { return }
304
301
  self.cachedCaptionsEnabled = enabled
305
- if self.isActive {
306
- self.bridge?.emit("onOverlayCaptionsEnabledChanged", body: [
307
- "surfaceId": self.surfaceId,
308
- "captionsEnabled": self.cachedCaptionsEnabled
309
- ])
310
- }
302
+ guard self.isActive, !self.isDragging else { return }
303
+ self.bridge?.emit("onOverlayCaptionsEnabledChanged", body: [
304
+ "surfaceId": self.surfaceId,
305
+ "captionsEnabled": self.cachedCaptionsEnabled
306
+ ])
311
307
  }
312
308
  .store(in: &cancellables)
313
309
 
@@ -315,30 +311,26 @@ import ShortKitSDK
315
311
  .receive(on: DispatchQueue.main)
316
312
  .sink { [weak self] cue in
317
313
  guard let self else { return }
314
+ // Pre-serialize once; reused by emitFullState() and individual emission.
318
315
  if let cue {
319
316
  self.cachedActiveCue = [
320
317
  "text": cue.text,
321
318
  "startTime": cue.startTime,
322
319
  "endTime": cue.endTime,
323
320
  ]
324
- } else {
325
- self.cachedActiveCue = nil
326
- }
327
- if self.isActive {
328
- if let cached = self.cachedActiveCue,
329
- let data = try? JSONSerialization.data(withJSONObject: cached),
321
+ if let data = try? JSONSerialization.data(withJSONObject: self.cachedActiveCue!),
330
322
  let json = String(data: data, encoding: .utf8) {
331
- self.bridge?.emit("onOverlayActiveCueChanged", body: [
332
- "surfaceId": self.surfaceId,
333
- "activeCue": json
334
- ])
335
- } else {
336
- self.bridge?.emit("onOverlayActiveCueChanged", body: [
337
- "surfaceId": self.surfaceId,
338
- "activeCue": NSNull()
339
- ])
323
+ self.cachedActiveCueJson = json
340
324
  }
325
+ } else {
326
+ self.cachedActiveCue = nil
327
+ self.cachedActiveCueJson = nil
341
328
  }
329
+ guard self.isActive, !self.isDragging else { return }
330
+ self.bridge?.emit("onOverlayActiveCueChanged", body: [
331
+ "surfaceId": self.surfaceId,
332
+ "activeCue": self.cachedActiveCueJson ?? NSNull()
333
+ ])
342
334
  }
343
335
  .store(in: &cancellables)
344
336
 
@@ -346,18 +338,26 @@ import ShortKitSDK
346
338
  .receive(on: DispatchQueue.main)
347
339
  .sink { [weak self] phase in
348
340
  guard let self else { return }
341
+ // Pre-serialize once; reused by emitFullState() and individual emission.
349
342
  switch phase {
350
343
  case .dragging(let from):
351
- let dict: [String: Any] = ["phase": "dragging", "fromId": from]
352
- if let data = try? JSONSerialization.data(withJSONObject: dict),
344
+ self.isDragging = true
345
+ if let data = try? JSONSerialization.data(withJSONObject: ["phase": "dragging", "fromId": from]),
353
346
  let json = String(data: data, encoding: .utf8) {
354
347
  self.cachedFeedScrollPhase = json
355
348
  }
356
349
  case .settled:
357
- let dict: [String: Any] = ["phase": "settled"]
358
- if let data = try? JSONSerialization.data(withJSONObject: dict),
359
- let json = String(data: data, encoding: .utf8) {
360
- self.cachedFeedScrollPhase = json
350
+ let wasDragging = self.isDragging
351
+ self.isDragging = false
352
+ self.cachedFeedScrollPhase = "{\"phase\":\"settled\"}"
353
+
354
+ // Re-sync all state that was suppressed during the drag.
355
+ // Handles both normal swipes (new item's activatePlayback
356
+ // will also fire) and cancelled swipes (same item, no
357
+ // activatePlayback — this is the only re-sync path).
358
+ if wasDragging, self.isActive {
359
+ self.emitFullState()
360
+ return // fullState includes feedScrollPhase
361
361
  }
362
362
  }
363
363
  if self.isActive {
@@ -372,7 +372,7 @@ import ShortKitSDK
372
372
  player.time
373
373
  .receive(on: DispatchQueue.main)
374
374
  .sink { [weak self] time in
375
- guard let self, self.isActive else { return }
375
+ guard let self, self.isActive, !self.isDragging else { return }
376
376
  self.cachedTime = (time.current, time.duration, time.buffered)
377
377
  self.timeDirty = true
378
378
  }
@@ -424,9 +424,7 @@ import ShortKitSDK
424
424
  props["playbackRate"] = cachedPlaybackRate
425
425
  props["captionsEnabled"] = cachedCaptionsEnabled
426
426
 
427
- if let cue = cachedActiveCue,
428
- let cueData = try? JSONSerialization.data(withJSONObject: cue),
429
- let cueJson = String(data: cueData, encoding: .utf8) {
427
+ if let cueJson = cachedActiveCueJson {
430
428
  props["activeCue"] = cueJson
431
429
  }
432
430
 
@@ -17,6 +17,15 @@ import ShortKitSDK
17
17
  private var shortKit: ShortKit?
18
18
  private var cancellables = Set<AnyCancellable>()
19
19
  private weak var delegate: ShortKitBridgeDelegateProtocol?
20
+ private var lastProgressEmitTime: CFTimeInterval = 0
21
+
22
+ // Time update coalescing (250ms, matching overlay timer interval)
23
+ private var cachedTime: (current: Double, duration: Double, buffered: Double) = (0, 0, 0)
24
+ private var timeCoalesceTimer: Timer?
25
+ private var timeDirty = false
26
+
27
+ // Scroll phase coalescing — only emit .dragging on first touch
28
+ private var lastEmittedScrollPhase: String = "settled"
20
29
 
21
30
  /// Surface presenter for creating RCTFabricSurface instances in overlay hosts.
22
31
  /// Set from ShortKitModule.mm via setSurfacePresenter: (called by RCTInstance).
@@ -116,12 +125,15 @@ import ShortKitSDK
116
125
  }
117
126
 
118
127
  subscribeToPublishers(sdk.player)
128
+ startTimeCoalescing()
119
129
  sdk.delegate = self
120
130
  }
121
131
 
122
132
  // MARK: - Teardown
123
133
 
124
134
  @objc public func teardown() {
135
+ timeCoalesceTimer?.invalidate()
136
+ timeCoalesceTimer = nil
125
137
  cancellables.removeAll()
126
138
  preloadHandles.removeAll()
127
139
  feedRegistry.removeAll()
@@ -376,24 +388,27 @@ import ShortKitSDK
376
388
  }
377
389
  .store(in: &cancellables)
378
390
 
379
- // Current item
391
+ // Current item — deferred to next run-loop tick so emission doesn't
392
+ // contend with Core Animation's commit phase during swipe settle.
380
393
  player.currentItem
381
394
  .receive(on: DispatchQueue.main)
382
395
  .sink { [weak self] item in
383
396
  guard let item else { return }
384
- self?.emit("onCurrentItemChanged", body: Self.contentItemDict(item))
397
+ let body = Self.contentItemDict(item)
398
+ DispatchQueue.main.async {
399
+ self?.emit("onCurrentItemChanged", body: body)
400
+ }
385
401
  }
386
402
  .store(in: &cancellables)
387
403
 
388
- // Time updates
404
+ // Time updates — coalesced at 250ms (4 events/sec) to avoid flooding
405
+ // the JS bridge. Matches the overlay timer interval in ReactOverlayHost.
389
406
  player.time
390
407
  .receive(on: DispatchQueue.main)
391
408
  .sink { [weak self] time in
392
- self?.emit("onTimeUpdate", body: [
393
- "current": time.current,
394
- "duration": time.duration,
395
- "buffered": time.buffered
396
- ])
409
+ guard let self else { return }
410
+ self.cachedTime = (time.current, time.duration, time.buffered)
411
+ self.timeDirty = true
397
412
  }
398
413
  .store(in: &cancellables)
399
414
 
@@ -458,7 +473,8 @@ import ShortKitSDK
458
473
  }
459
474
  .store(in: &cancellables)
460
475
 
461
- // Feed transition
476
+ // Feed transition — deferred to next run-loop tick so emission doesn't
477
+ // contend with Core Animation's commit phase during swipe settle.
462
478
  player.feedTransition
463
479
  .receive(on: DispatchQueue.main)
464
480
  .sink { [weak self] event in
@@ -473,24 +489,35 @@ import ShortKitSDK
473
489
  if let to = event.to {
474
490
  body["toItem"] = self.serializeContentItemToJSON(to)
475
491
  }
476
- self.emit("onFeedTransition", body: body)
492
+ DispatchQueue.main.async { [weak self] in
493
+ self?.emit("onFeedTransition", body: body)
494
+ }
477
495
  }
478
496
  .store(in: &cancellables)
479
497
 
480
- // Feed scroll phase
498
+ // Feed scroll phase — coalesced: only emit .dragging on first touch
499
+ // (transition from settled), drop intermediate .dragging events. Always
500
+ // emit .settled. Deferred to next run-loop tick to avoid contending with
501
+ // Core Animation's commit phase.
481
502
  player.feedScrollPhase
482
503
  .receive(on: DispatchQueue.main)
483
504
  .sink { [weak self] phase in
505
+ guard let self else { return }
484
506
  switch phase {
485
507
  case .dragging(let fromId):
486
- self?.emit("onFeedScrollPhase", body: [
487
- "phase": "dragging",
488
- "fromId": fromId
489
- ])
508
+ // Only emit on transition from settled → dragging
509
+ guard self.lastEmittedScrollPhase != "dragging" else { return }
510
+ self.lastEmittedScrollPhase = "dragging"
511
+ let body: [String: Any] = ["phase": "dragging", "fromId": fromId]
512
+ DispatchQueue.main.async { [weak self] in
513
+ self?.emit("onFeedScrollPhase", body: body)
514
+ }
490
515
  case .settled:
491
- self?.emit("onFeedScrollPhase", body: [
492
- "phase": "settled"
493
- ])
516
+ self.lastEmittedScrollPhase = "settled"
517
+ let body: [String: Any] = ["phase": "settled"]
518
+ DispatchQueue.main.async { [weak self] in
519
+ self?.emit("onFeedScrollPhase", body: body)
520
+ }
494
521
  }
495
522
  }
496
523
  .store(in: &cancellables)
@@ -536,6 +563,21 @@ import ShortKitSDK
536
563
  }
537
564
  }
538
565
 
566
+ // MARK: - Time Coalescing
567
+
568
+ private func startTimeCoalescing() {
569
+ timeCoalesceTimer?.invalidate()
570
+ timeCoalesceTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
571
+ guard let self, self.timeDirty else { return }
572
+ self.timeDirty = false
573
+ self.emit("onTimeUpdate", body: [
574
+ "current": self.cachedTime.current,
575
+ "duration": self.cachedTime.duration,
576
+ "buffered": self.cachedTime.buffered
577
+ ])
578
+ }
579
+ }
580
+
539
581
  // MARK: - Content Item Serialization
540
582
 
541
583
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -658,6 +700,8 @@ import ShortKitSDK
658
700
  let scrollAxisStr = obj["scrollAxis"] as? String ?? "vertical"
659
701
  let scrollAxis: ScrollAxis = scrollAxisStr == "horizontal" ? .horizontal : .vertical
660
702
 
703
+ let pullToRefreshEnabled = obj["pullToRefreshEnabled"] as? Bool ?? true
704
+
661
705
  return FeedConfig(
662
706
  feedHeight: feedHeight,
663
707
  scrollAxis: scrollAxis,
@@ -669,6 +713,7 @@ import ShortKitSDK
669
713
  muteOnStart: muteOnStart,
670
714
  autoplay: autoplay,
671
715
  feedSource: feedSource,
716
+ pullToRefreshEnabled: pullToRefreshEnabled,
672
717
  filter: filter
673
718
  )
674
719
  }
@@ -870,8 +915,23 @@ extension ShortKitBridge: ShortKitDelegate {
870
915
  ])
871
916
  }
872
917
 
873
- public func shortKitDidRequestRefresh(_ shortKit: ShortKit) {
874
- emitOnMain("onRefreshRequested", body: [:])
918
+ public func shortKit(_ shortKit: ShortKit, didChangeRefreshState state: ShortKitRefreshState) {
919
+ let body: [String: Any]
920
+ switch state {
921
+ case .idle:
922
+ body = ["status": "idle", "progress": 0.0]
923
+ case .pulling(let progress):
924
+ // Throttle pulling events to max 1 per 16ms to avoid bridge saturation
925
+ let now = CACurrentMediaTime()
926
+ guard now - lastProgressEmitTime >= 0.016 else { return }
927
+ lastProgressEmitTime = now
928
+ body = ["status": "pulling", "progress": progress]
929
+ case .triggered:
930
+ body = ["status": "triggered", "progress": 0.0]
931
+ case .refreshing:
932
+ body = ["status": "refreshing", "progress": 0.0]
933
+ }
934
+ emitOnMain("onRefreshStateChanged", body: body)
875
935
  }
876
936
 
877
937
  public func shortKit(_ shortKit: ShortKit, didFetchContentItems items: [ContentItem]) {
@@ -72,7 +72,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
72
72
  @"onContentTapped",
73
73
  @"onDismiss",
74
74
  @"onFeedReady",
75
- @"onRefreshRequested",
75
+ @"onRefreshStateChanged",
76
76
  @"onDidFetchContentItems",
77
77
  @"onOverlayActiveChanged",
78
78
  @"onOverlayPlayerStateChanged",
@@ -264,9 +264,18 @@ RCT_EXPORT_METHOD(preloadFeed:(NSString *)configJSON
264
264
  itemsJSON:(NSString *)itemsJSON
265
265
  resolve:(RCTPromiseResolveBlock)resolve
266
266
  reject:(RCTPromiseRejectBlock)reject) {
267
- [_shortKitBridge preloadFeed:configJSON itemsJSON:itemsJSON completion:^(NSString *uuid) {
268
- resolve(uuid);
269
- }];
267
+ if (_shortKitBridge) {
268
+ [_shortKitBridge preloadFeed:configJSON itemsJSON:itemsJSON completion:^(NSString *uuid) {
269
+ resolve(uuid);
270
+ }];
271
+ } else {
272
+ if (!_pendingBridgeOps) _pendingBridgeOps = [NSMutableArray new];
273
+ [_pendingBridgeOps addObject:^{
274
+ [self->_shortKitBridge preloadFeed:configJSON itemsJSON:itemsJSON completion:^(NSString *uuid) {
275
+ resolve(uuid);
276
+ }];
277
+ }];
278
+ }
270
279
  }
271
280
 
272
281
  // MARK: - Storyboard / Seek Thumbnails
@@ -22,12 +22,13 @@
22
22
  <key>BinaryPath</key>
23
23
  <string>ShortKitSDK.framework/ShortKitSDK</string>
24
24
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64-simulator</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
26
26
  <key>LibraryPath</key>
27
27
  <string>ShortKitSDK.framework</string>
28
28
  <key>SupportedArchitectures</key>
29
29
  <array>
30
30
  <string>arm64</string>
31
+ <string>x86_64</string>
31
32
  </array>
32
33
  <key>SupportedPlatform</key>
33
34
  <string>ios</string>
@@ -360,6 +360,7 @@ SWIFT_CLASS("_TtC11ShortKitSDK26ShortKitFeedViewController")
360
360
  - (void)scrollViewWillBeginDragging:(UIScrollView * _Nonnull)scrollView;
361
361
  - (void)scrollViewDidEndDecelerating:(UIScrollView * _Nonnull)scrollView;
362
362
  - (void)scrollViewDidEndDragging:(UIScrollView * _Nonnull)scrollView willDecelerate:(BOOL)decelerate;
363
+ - (void)scrollViewDidScrollToTop:(UIScrollView * _Nonnull)scrollView;
363
364
  - (void)scrollViewDidScroll:(UIScrollView * _Nonnull)scrollView;
364
365
  @end
365
366
 
@@ -11,6 +11,10 @@
11
11
  <key>CFBundlePackageType</key>
12
12
  <string>FMWK</string>
13
13
  <key>CFBundleVersion</key>
14
- <string>1</string>
14
+ <string>0.2.15</string>
15
+ <key>CFBundleShortVersionString</key>
16
+ <string>0.2.15</string>
17
+ <key>MinimumOSVersion</key>
18
+ <string>16.0</string>
15
19
  </dict>
16
20
  </plist>