@shortkitsdk/react-native 0.2.1 → 0.2.3

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 (40) hide show
  1. package/ShortKitReactNative.podspec +8 -1
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +67 -5
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
  6. package/ios/ShortKitBridge.swift +47 -2
  7. package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
  8. package/ios/ShortKitFeedView.swift +47 -8
  9. package/ios/ShortKitModule.mm +22 -2
  10. package/ios/ShortKitOverlayBridge.swift +24 -2
  11. package/ios/ShortKitPlayerNativeView.swift +1 -1
  12. package/ios/ShortKitSDK.xcframework/Info.plist +43 -0
  13. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +417 -0
  14. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  15. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +27739 -0
  16. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +790 -0
  17. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  18. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +790 -0
  19. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  20. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  21. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +417 -0
  22. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +27739 -0
  24. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +790 -0
  25. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  26. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +790 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  28. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitWidgetNativeView.swift +1 -1
  30. package/package.json +1 -1
  31. package/src/CarouselOverlayManager.tsx +71 -0
  32. package/src/ShortKitContext.ts +13 -0
  33. package/src/ShortKitFeed.tsx +3 -0
  34. package/src/ShortKitProvider.tsx +118 -2
  35. package/src/index.ts +3 -1
  36. package/src/serialization.ts +6 -2
  37. package/src/specs/NativeShortKitModule.ts +19 -0
  38. package/src/types.ts +4 -3
  39. package/src/useShortKitCarousel.ts +29 -0
  40. package/src/useShortKitPlayer.ts +10 -2
@@ -13,7 +13,14 @@ Pod::Spec.new do |s|
13
13
  s.source_files = "ios/*.{h,m,mm,cpp,swift}"
14
14
  s.requires_arc = true
15
15
 
16
- s.dependency "ShortKit"
16
+ # When the vendored XCFramework is present (npm published package), use it.
17
+ # Otherwise fall back to the local ShortKit pod (development).
18
+ xcframework_path = File.join(__dir__, "ios", "ShortKitSDK.xcframework")
19
+ if File.exist?(xcframework_path)
20
+ s.vendored_frameworks = "ios/ShortKitSDK.xcframework"
21
+ else
22
+ s.dependency "ShortKit"
23
+ end
17
24
 
18
25
  install_modules_dependencies(s)
19
26
  end
@@ -0,0 +1,48 @@
1
+ package com.shortkit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.widget.FrameLayout
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.shortkit.sdk.model.ImageCarouselItem
8
+ import com.shortkit.sdk.overlay.CarouselOverlay
9
+ import kotlinx.serialization.encodeToString
10
+ import kotlinx.serialization.json.Json
11
+
12
+ /**
13
+ * A transparent [FrameLayout] that implements [CarouselOverlay] and bridges
14
+ * carousel overlay lifecycle calls to JS events via [ShortKitModule].
15
+ *
16
+ * The actual carousel overlay UI is rendered by React Native on the JS side
17
+ * through the `CarouselOverlayManager` component. This view simply relays
18
+ * the SDK lifecycle events so the JS carousel overlay knows when to
19
+ * configure, activate, reset, etc.
20
+ *
21
+ * Android equivalent of iOS `ShortKitCarouselOverlayBridge.swift`.
22
+ */
23
+ class ShortKitCarouselOverlayBridge(context: Context) : FrameLayout(context), CarouselOverlay {
24
+
25
+ init {
26
+ setBackgroundColor(Color.TRANSPARENT)
27
+ }
28
+
29
+ override fun configure(item: ImageCarouselItem) {
30
+ val json = Json.encodeToString(item)
31
+ val params = Arguments.createMap().apply {
32
+ putString("item", json)
33
+ }
34
+ ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayConfigure", params)
35
+ }
36
+
37
+ override fun resetState() {
38
+ ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayReset", Arguments.createMap())
39
+ }
40
+
41
+ override fun fadeOutForTransition() {
42
+ ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayFadeOut", Arguments.createMap())
43
+ }
44
+
45
+ override fun restoreFromTransition() {
46
+ ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayRestore", Arguments.createMap())
47
+ }
48
+ }
@@ -51,12 +51,18 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
51
51
  private var viewPager: ViewPager2? = null
52
52
  private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
53
53
 
54
- /** Overlay for the currently active cell (nativeID="overlay-current"). */
54
+ /** Video overlay for the currently active cell (nativeID="overlay-current"). */
55
55
  private var currentOverlayView: View? = null
56
56
 
57
- /** Overlay for the upcoming cell (nativeID="overlay-next"). */
57
+ /** Video overlay for the upcoming cell (nativeID="overlay-next"). */
58
58
  private var nextOverlayView: View? = null
59
59
 
60
+ /** Carousel overlay for the currently active cell (nativeID="carousel-overlay-current"). */
61
+ private var currentCarouselOverlayView: View? = null
62
+
63
+ /** Carousel overlay for the upcoming cell (nativeID="carousel-overlay-next"). */
64
+ private var nextCarouselOverlayView: View? = null
65
+
60
66
  /** The page index used for overlay transform calculations. */
61
67
  private var currentPage: Int = 0
62
68
 
@@ -158,8 +164,12 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
158
164
  viewPager = null
159
165
  currentOverlayView?.translationY = 0f
160
166
  nextOverlayView?.translationY = 0f
167
+ currentCarouselOverlayView?.translationY = 0f
168
+ nextCarouselOverlayView?.translationY = 0f
161
169
  currentOverlayView = null
162
170
  nextOverlayView = null
171
+ currentCarouselOverlayView = null
172
+ nextCarouselOverlayView = null
163
173
  }
164
174
 
165
175
  private fun handleScrollOffset(
@@ -193,6 +203,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
193
203
  val h = height.toFloat()
194
204
  currentOverlayView?.translationY = 0f
195
205
  nextOverlayView?.translationY = h
206
+ currentCarouselOverlayView?.translationY = 0f
207
+ nextCarouselOverlayView?.translationY = h
196
208
  }
197
209
  pageUpdateRunnable = runnable
198
210
  handler.postDelayed(runnable, 80)
@@ -219,17 +231,25 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
219
231
  if (currentOverlayView == null || nextOverlayView == null) {
220
232
  findOverlayViews()
221
233
  }
234
+ if (currentCarouselOverlayView == null || nextCarouselOverlayView == null) {
235
+ findCarouselOverlayViews()
236
+ }
222
237
 
223
- // Current overlay follows the active cell
238
+ // Current overlays follow the active cell
224
239
  currentOverlayView?.translationY = -delta
240
+ currentCarouselOverlayView?.translationY = -delta
225
241
 
226
- // Next overlay: positioned one page ahead in the scroll direction
242
+ // Next overlays: positioned one page ahead in the scroll direction
227
243
  if (delta >= 0) {
228
244
  // Forward scroll (or idle): next cell is below
229
- nextOverlayView?.translationY = cellHeight - delta
245
+ val nextY = cellHeight - delta
246
+ nextOverlayView?.translationY = nextY
247
+ nextCarouselOverlayView?.translationY = nextY
230
248
  } else {
231
249
  // Backward scroll: next cell is above
232
- nextOverlayView?.translationY = -cellHeight - delta
250
+ val nextY = -cellHeight - delta
251
+ nextOverlayView?.translationY = nextY
252
+ nextCarouselOverlayView?.translationY = nextY
233
253
  }
234
254
  }
235
255
 
@@ -260,6 +280,28 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
260
280
  }
261
281
  }
262
282
 
283
+ /**
284
+ * Find the sibling RN carousel overlay views by nativeID.
285
+ */
286
+ private fun findCarouselOverlayViews() {
287
+ var ancestor = parent as? ViewGroup ?: return
288
+ while (true) {
289
+ for (i in 0 until ancestor.childCount) {
290
+ val child = ancestor.getChildAt(i)
291
+ if (child === this || isOwnAncestor(child)) continue
292
+
293
+ if (currentCarouselOverlayView == null) {
294
+ currentCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-current")
295
+ }
296
+ if (nextCarouselOverlayView == null) {
297
+ nextCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-next")
298
+ }
299
+ }
300
+ if (currentCarouselOverlayView != null && nextCarouselOverlayView != null) return
301
+ ancestor = ancestor.parent as? ViewGroup ?: return
302
+ }
303
+ }
304
+
263
305
  /** Check if the given view is an ancestor of this view. */
264
306
  private fun isOwnAncestor(view: View): Boolean {
265
307
  var current: ViewParent? = parent
@@ -50,6 +50,8 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
50
50
  private var listenerCount = 0
51
51
  @Volatile
52
52
  private var hasListeners = false
53
+ private var pendingFeedItems: String? = null
54
+ private var pendingAppendItems: String? = null
53
55
 
54
56
  /** Expose the underlying SDK for the Fabric feed view manager. */
55
57
  val sdk: ShortKit? get() = shortKit
@@ -120,6 +122,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
120
122
  this.shortKit = sdk
121
123
  shared = this
122
124
 
125
+ // Replay any feed items that arrived before initialization
126
+ pendingFeedItems?.let { json ->
127
+ parseCustomFeedItems(json)?.let { sdk.setFeedItems(it) }
128
+ pendingFeedItems = null
129
+ }
130
+ pendingAppendItems?.let { json ->
131
+ parseCustomFeedItems(json)?.let { sdk.appendFeedItems(it) }
132
+ pendingAppendItems = null
133
+ }
134
+
123
135
  subscribeToFlows(sdk.player)
124
136
 
125
137
  sdk.delegate = object : com.shortkit.ShortKitDelegate {
@@ -233,14 +245,24 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
233
245
 
234
246
  @ReactMethod
235
247
  override fun setFeedItems(items: String) {
236
- val parsed = parseCustomFeedItems(items) ?: return
237
- shortKit?.setFeedItems(parsed)
248
+ val sdk = shortKit
249
+ if (sdk != null) {
250
+ val parsed = parseCustomFeedItems(items) ?: return
251
+ sdk.setFeedItems(parsed)
252
+ } else {
253
+ pendingFeedItems = items
254
+ }
238
255
  }
239
256
 
240
257
  @ReactMethod
241
258
  override fun appendFeedItems(items: String) {
242
- val parsed = parseCustomFeedItems(items) ?: return
243
- shortKit?.appendFeedItems(parsed)
259
+ val sdk = shortKit
260
+ if (sdk != null) {
261
+ val parsed = parseCustomFeedItems(items) ?: return
262
+ sdk.appendFeedItems(parsed)
263
+ } else {
264
+ pendingAppendItems = items
265
+ }
244
266
  }
245
267
 
246
268
  @ReactMethod
@@ -279,6 +301,14 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
279
301
  sendEvent(name, params)
280
302
  }
281
303
 
304
+ // -----------------------------------------------------------------------
305
+ // Carousel overlay lifecycle events
306
+ // -----------------------------------------------------------------------
307
+
308
+ fun emitCarouselOverlayEvent(name: String, params: WritableMap) {
309
+ sendEvent(name, params)
310
+ }
311
+
282
312
  // -----------------------------------------------------------------------
283
313
  // Flow subscriptions
284
314
  // -----------------------------------------------------------------------
@@ -642,10 +672,12 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
642
672
  val feedSourceStr = obj.optString("feedSource", "algorithmic")
643
673
  val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
644
674
 
675
+ val carouselOverlay = parseCarouselOverlay(obj.optString("carouselOverlay", null))
676
+
645
677
  FeedConfig(
646
678
  feedHeight = feedHeight,
647
679
  videoOverlay = videoOverlay,
648
- carouselOverlay = CarouselOverlayMode.None,
680
+ carouselOverlay = carouselOverlay,
649
681
  surveyOverlay = SurveyOverlayMode.None,
650
682
  adOverlay = AdOverlayMode.None,
651
683
  muteOnStart = muteOnStart,
@@ -712,6 +744,36 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
712
744
  }
713
745
  }
714
746
 
747
+ /**
748
+ * Parse a double-stringified carousel overlay JSON.
749
+ * - `"\"none\""` -> None
750
+ * - `"{\"type\":\"custom\"}"` -> Custom with bridge overlay factory
751
+ */
752
+ private fun parseCarouselOverlay(json: String?): CarouselOverlayMode {
753
+ if (json.isNullOrEmpty()) return CarouselOverlayMode.None
754
+ return try {
755
+ val parsed = json.trim()
756
+
757
+ if (parsed == "\"none\"" || parsed == "none") {
758
+ return CarouselOverlayMode.None
759
+ }
760
+
761
+ val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
762
+ JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
763
+ } else {
764
+ JSONObject(parsed)
765
+ }
766
+
767
+ if (inner.optString("type") == "custom") {
768
+ CarouselOverlayMode.Custom { ShortKitCarouselOverlayBridge(reactApplicationContext) }
769
+ } else {
770
+ CarouselOverlayMode.None
771
+ }
772
+ } catch (_: Exception) {
773
+ CarouselOverlayMode.None
774
+ }
775
+ }
776
+
715
777
  private fun parseCustomFeedItems(json: String): List<CustomFeedItem>? {
716
778
  return try {
717
779
  val arr = JSONArray(json)
@@ -2,6 +2,8 @@ package com.shortkit.reactnative
2
2
 
3
3
  import android.content.Context
4
4
  import android.graphics.Color
5
+ import android.os.Handler
6
+ import android.os.Looper
5
7
  import android.view.GestureDetector
6
8
  import android.view.MotionEvent
7
9
  import android.widget.FrameLayout
@@ -32,6 +34,15 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
32
34
  */
33
35
  private var currentItem: ContentItem? = null
34
36
 
37
+ /**
38
+ * Deferred configure emission — cancelled if [activatePlayback] fires
39
+ * on the same message-queue iteration (handleSwipe re-configure of the
40
+ * active cell, not a prefetch for the next cell).
41
+ */
42
+ private var pendingConfigureRunnable: Runnable? = null
43
+
44
+ private val handler = Handler(Looper.getMainLooper())
45
+
35
46
  // -----------------------------------------------------------------------
36
47
  // Gestures
37
48
  // -----------------------------------------------------------------------
@@ -76,7 +87,18 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
76
87
 
77
88
  override fun configure(item: ContentItem) {
78
89
  currentItem = item
79
- ShortKitModule.shared?.emitOverlayEvent("onOverlayConfigure", item)
90
+
91
+ // Defer the configure event by one message-queue tick. If activatePlayback()
92
+ // fires before then (handleSwipe sequence: configure → reset → activate),
93
+ // the event is cancelled — preventing nextItem from being overwritten
94
+ // with the current cell's data.
95
+ pendingConfigureRunnable?.let { handler.removeCallbacks(it) }
96
+ val runnable = Runnable {
97
+ currentItem?.let { ShortKitModule.shared?.emitOverlayEvent("onOverlayConfigure", it) }
98
+ pendingConfigureRunnable = null
99
+ }
100
+ pendingConfigureRunnable = runnable
101
+ handler.post(runnable)
80
102
  }
81
103
 
82
104
  override fun resetPlaybackProgress() {
@@ -85,6 +107,11 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
85
107
  }
86
108
 
87
109
  override fun activatePlayback() {
110
+ // Cancel the pending configure — it was for the current cell (part of
111
+ // handleSwipe), not a prefetch for the next cell.
112
+ pendingConfigureRunnable?.let { handler.removeCallbacks(it) }
113
+ pendingConfigureRunnable = null
114
+
88
115
  val item = currentItem ?: return
89
116
  ShortKitModule.shared?.emitOverlayEvent("onOverlayActivate", item)
90
117
  }
@@ -1,6 +1,6 @@
1
1
  import Foundation
2
2
  import Combine
3
- import ShortKit
3
+ import ShortKitSDK
4
4
 
5
5
  /// Swift bridge between the ShortKit SDK and the Obj-C++ TurboModule.
6
6
  ///
@@ -347,6 +347,20 @@ import ShortKit
347
347
  emitOnMain(name, body: body)
348
348
  }
349
349
 
350
+ // MARK: - Carousel Overlay Lifecycle Events
351
+
352
+ /// Emit carousel overlay lifecycle events with an ImageCarouselItem.
353
+ public func emitCarouselOverlayEvent(_ name: String, item: ImageCarouselItem) {
354
+ guard let data = try? JSONEncoder().encode(item),
355
+ let json = String(data: data, encoding: .utf8) else { return }
356
+ emitOnMain(name, body: ["item": json])
357
+ }
358
+
359
+ /// Emit a raw carousel overlay event with an arbitrary body.
360
+ public func emitCarouselOverlayEvent(_ name: String, body: [String: Any]) {
361
+ emitOnMain(name, body: body)
362
+ }
363
+
350
364
  // MARK: - Content Item Serialization
351
365
 
352
366
  /// Serialize a ContentItem to a JSON string for bridge transport.
@@ -457,10 +471,12 @@ import ShortKit
457
471
  let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
458
472
  let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
459
473
 
474
+ let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
475
+
460
476
  return FeedConfig(
461
477
  feedHeight: feedHeight,
462
478
  videoOverlay: videoOverlay,
463
- carouselOverlay: .none,
479
+ carouselOverlay: carouselOverlay,
464
480
  surveyOverlay: .none,
465
481
  adOverlay: .none,
466
482
  muteOnStart: muteOnStart,
@@ -499,6 +515,35 @@ import ShortKit
499
515
  return .none
500
516
  }
501
517
 
518
+ /// Parse a double-stringified carousel overlay JSON into a `CarouselOverlayMode`.
519
+ ///
520
+ /// Examples:
521
+ /// - `"\"none\""` → `.none`
522
+ /// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitCarouselOverlayBridge() }`
523
+ private static func parseCarouselOverlay(_ json: String?) -> CarouselOverlayMode {
524
+ guard let json,
525
+ let data = json.data(using: .utf8),
526
+ let parsed = try? JSONSerialization.jsonObject(with: data) else {
527
+ return .none
528
+ }
529
+
530
+ if let str = parsed as? String, str == "none" {
531
+ return .none
532
+ }
533
+
534
+ if let obj = parsed as? [String: Any],
535
+ let type = obj["type"] as? String,
536
+ type == "custom" {
537
+ return .custom { @Sendable in
538
+ let overlay = ShortKitCarouselOverlayBridge()
539
+ overlay.bridge = ShortKitBridge.shared
540
+ return overlay
541
+ }
542
+ }
543
+
544
+ return .none
545
+ }
546
+
502
547
  /// Parse a double-stringified feedHeight JSON.
503
548
  /// e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
504
549
  private static func parseFeedHeight(_ json: String?) -> FeedHeight {
@@ -0,0 +1,54 @@
1
+ import UIKit
2
+ import ShortKitSDK
3
+
4
+ /// A transparent UIView that conforms to `CarouselOverlay` and bridges
5
+ /// carousel overlay lifecycle calls to JS events via `ShortKitBridge`.
6
+ ///
7
+ /// The actual carousel overlay UI is rendered by React Native on the JS side
8
+ /// through the `CarouselOverlayManager` component. This view simply relays
9
+ /// the SDK lifecycle events so the JS carousel overlay knows when to
10
+ /// configure, activate, reset, etc.
11
+ @objc public class ShortKitCarouselOverlayBridge: UIView, @unchecked Sendable, CarouselOverlay {
12
+
13
+ // MARK: - Bridge Reference
14
+
15
+ /// Weak reference to the bridge, set by the factory closure in `parseFeedConfig`.
16
+ weak var bridge: ShortKitBridge?
17
+
18
+ // MARK: - State
19
+
20
+ /// Stores the last configured ImageCarouselItem so we can pass it with
21
+ /// lifecycle events that don't receive the item as a parameter.
22
+ private var currentItem: ImageCarouselItem?
23
+
24
+ // MARK: - Init
25
+
26
+ override init(frame: CGRect) {
27
+ super.init(frame: frame)
28
+ backgroundColor = .clear
29
+ isUserInteractionEnabled = true
30
+ }
31
+
32
+ required init?(coder: NSCoder) {
33
+ fatalError("init(coder:) is not supported")
34
+ }
35
+
36
+ // MARK: - CarouselOverlay
37
+
38
+ public func configure(with item: ImageCarouselItem) {
39
+ currentItem = item
40
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayConfigure", item: item)
41
+ }
42
+
43
+ public func resetState() {
44
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayReset", body: [:])
45
+ }
46
+
47
+ public func fadeOutForTransition() {
48
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayFadeOut", body: [:])
49
+ }
50
+
51
+ public func restoreFromTransition() {
52
+ bridge?.emitCarouselOverlayEvent("onCarouselOverlayRestore", body: [:])
53
+ }
54
+ }
@@ -1,5 +1,5 @@
1
1
  import UIKit
2
- import ShortKit
2
+ import ShortKitSDK
3
3
 
4
4
  /// Fabric native view that embeds `ShortKitFeedViewController` using
5
5
  /// UIViewController containment. Props are set by the view manager via
@@ -27,10 +27,14 @@ import ShortKit
27
27
  // MARK: - Scroll Tracking
28
28
 
29
29
  private var scrollObservation: NSKeyValueObservation?
30
- /// Overlay for the currently active cell (nativeID="overlay-current").
30
+ /// Video overlay for the currently active cell (nativeID="overlay-current").
31
31
  private weak var currentOverlayView: UIView?
32
- /// Overlay for the upcoming cell (nativeID="overlay-next").
32
+ /// Video overlay for the upcoming cell (nativeID="overlay-next").
33
33
  private weak var nextOverlayView: UIView?
34
+ /// Carousel overlay for the currently active cell (nativeID="carousel-overlay-current").
35
+ private weak var currentCarouselOverlayView: UIView?
36
+ /// Carousel overlay for the upcoming cell (nativeID="carousel-overlay-next").
37
+ private weak var nextCarouselOverlayView: UIView?
34
38
  /// The page index used for overlay transform calculations.
35
39
  private var currentPage: Int = 0
36
40
  /// Deferred page update to avoid flashing stale metadata.
@@ -94,8 +98,12 @@ import ShortKit
94
98
  scrollObservation = nil
95
99
  currentOverlayView?.transform = .identity
96
100
  nextOverlayView?.transform = .identity
101
+ currentCarouselOverlayView?.transform = .identity
102
+ nextCarouselOverlayView?.transform = .identity
97
103
  currentOverlayView = nil
98
104
  nextOverlayView = nil
105
+ currentCarouselOverlayView = nil
106
+ nextCarouselOverlayView = nil
99
107
 
100
108
  guard let feedVC = feedViewController else { return }
101
109
 
@@ -150,6 +158,8 @@ import ShortKit
150
158
  let h = self.bounds.height
151
159
  self.currentOverlayView?.transform = .identity
152
160
  self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
161
+ self.currentCarouselOverlayView?.transform = .identity
162
+ self.nextCarouselOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
153
163
  }
154
164
  pageUpdateWorkItem = workItem
155
165
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
@@ -168,17 +178,27 @@ import ShortKit
168
178
  if currentOverlayView == nil || nextOverlayView == nil {
169
179
  findOverlayViews()
170
180
  }
181
+ if currentCarouselOverlayView == nil || nextCarouselOverlayView == nil {
182
+ findCarouselOverlayViews()
183
+ }
184
+
185
+ let translateY = CGAffineTransform(translationX: 0, y: -delta)
171
186
 
172
- // Current overlay follows the active cell
173
- currentOverlayView?.transform = CGAffineTransform(translationX: 0, y: -delta)
187
+ // Current overlays follow the active cell
188
+ currentOverlayView?.transform = translateY
189
+ currentCarouselOverlayView?.transform = translateY
174
190
 
175
- // Next overlay: positioned one page ahead in the scroll direction
191
+ // Next overlays: positioned one page ahead in the scroll direction
176
192
  if delta >= 0 {
177
193
  // Forward scroll (or idle): next cell is below
178
- nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
194
+ let nextTransform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
195
+ nextOverlayView?.transform = nextTransform
196
+ nextCarouselOverlayView?.transform = nextTransform
179
197
  } else {
180
198
  // Backward scroll: next cell is above
181
- nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
199
+ let nextTransform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
200
+ nextOverlayView?.transform = nextTransform
201
+ nextCarouselOverlayView?.transform = nextTransform
182
202
  }
183
203
  }
184
204
 
@@ -208,6 +228,25 @@ import ShortKit
208
228
  }
209
229
  }
210
230
 
231
+ /// Find the sibling RN carousel overlay views by nativeID.
232
+ private func findCarouselOverlayViews() {
233
+ var ancestor: UIView? = superview
234
+ while let container = ancestor {
235
+ for child in container.subviews {
236
+ guard !self.isDescendant(of: child) else { continue }
237
+
238
+ if currentCarouselOverlayView == nil {
239
+ currentCarouselOverlayView = findView(withNativeID: "carousel-overlay-current", in: child)
240
+ }
241
+ if nextCarouselOverlayView == nil {
242
+ nextCarouselOverlayView = findView(withNativeID: "carousel-overlay-next", in: child)
243
+ }
244
+ }
245
+ if currentCarouselOverlayView != nil && nextCarouselOverlayView != nil { return }
246
+ ancestor = container.superview
247
+ }
248
+ }
249
+
211
250
  /// Recursively find a view by its React Native `nativeID` prop.
212
251
  /// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
213
252
  /// not `accessibilityIdentifier`.
@@ -11,6 +11,8 @@
11
11
  @implementation ShortKitModule {
12
12
  ShortKitBridge *_shortKitBridge;
13
13
  BOOL _hasListeners;
14
+ NSString *_pendingFeedItems;
15
+ NSString *_pendingAppendItems;
14
16
  }
15
17
 
16
18
  RCT_EXPORT_MODULE(ShortKitModule)
@@ -118,6 +120,16 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
118
120
  clientAppVersion:clientAppVersion
119
121
  customDimensions:customDimensions
120
122
  delegate:self];
123
+
124
+ // Replay any feed items that arrived before initialization
125
+ if (_pendingFeedItems) {
126
+ [_shortKitBridge setFeedItems:_pendingFeedItems];
127
+ _pendingFeedItems = nil;
128
+ }
129
+ if (_pendingAppendItems) {
130
+ [_shortKitBridge appendFeedItems:_pendingAppendItems];
131
+ _pendingAppendItems = nil;
132
+ }
121
133
  }
122
134
 
123
135
  RCT_EXPORT_METHOD(destroy) {
@@ -194,11 +206,19 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
194
206
  // MARK: - Custom Feed
195
207
 
196
208
  RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
197
- [_shortKitBridge setFeedItems:items];
209
+ if (_shortKitBridge) {
210
+ [_shortKitBridge setFeedItems:items];
211
+ } else {
212
+ _pendingFeedItems = items;
213
+ }
198
214
  }
199
215
 
200
216
  RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
201
- [_shortKitBridge appendFeedItems:items];
217
+ if (_shortKitBridge) {
218
+ [_shortKitBridge appendFeedItems:items];
219
+ } else {
220
+ _pendingAppendItems = items;
221
+ }
202
222
  }
203
223
 
204
224
  RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
@@ -1,5 +1,5 @@
1
1
  import UIKit
2
- import ShortKit
2
+ import ShortKitSDK
3
3
 
4
4
  /// A transparent UIView that conforms to `FeedOverlay` and bridges overlay
5
5
  /// lifecycle calls to JS events via `ShortKitBridge`.
@@ -20,6 +20,11 @@ import ShortKit
20
20
  /// events that don't receive the item as a parameter.
21
21
  private var currentItem: ContentItem?
22
22
 
23
+ /// Deferred configure emission — cancelled if `activatePlayback()` fires
24
+ /// on the same run-loop iteration (meaning this was a handleSwipe
25
+ /// re-configure of the active cell, not a prefetch for the next cell).
26
+ private var pendingConfigureWorkItem: DispatchWorkItem?
27
+
23
28
  // MARK: - Init
24
29
 
25
30
  override init(frame: CGRect) {
@@ -66,7 +71,19 @@ import ShortKit
66
71
 
67
72
  public func configure(with item: ContentItem) {
68
73
  currentItem = item
69
- bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
74
+
75
+ // Defer the configure event by one run-loop tick. If activatePlayback()
76
+ // fires before then (handleSwipe sequence: configure → reset → activate),
77
+ // the event is cancelled — preventing nextItem from being overwritten
78
+ // with the current cell's data.
79
+ pendingConfigureWorkItem?.cancel()
80
+ let workItem = DispatchWorkItem { [weak self] in
81
+ guard let self, let item = self.currentItem else { return }
82
+ self.bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
83
+ self.pendingConfigureWorkItem = nil
84
+ }
85
+ pendingConfigureWorkItem = workItem
86
+ DispatchQueue.main.async(execute: workItem)
70
87
  }
71
88
 
72
89
  public func resetPlaybackProgress() {
@@ -75,6 +92,11 @@ import ShortKit
75
92
  }
76
93
 
77
94
  public func activatePlayback() {
95
+ // Cancel the pending configure — it was for the current cell (part of
96
+ // handleSwipe), not a prefetch for the next cell.
97
+ pendingConfigureWorkItem?.cancel()
98
+ pendingConfigureWorkItem = nil
99
+
78
100
  guard let item = currentItem else { return }
79
101
  bridge?.emitOverlayEvent("onOverlayActivate", item: item)
80
102
  }