@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.
- package/ShortKitReactNative.podspec +8 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +67 -5
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
- package/ios/ShortKitBridge.swift +47 -2
- package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
- package/ios/ShortKitFeedView.swift +47 -8
- package/ios/ShortKitModule.mm +22 -2
- package/ios/ShortKitOverlayBridge.swift +24 -2
- package/ios/ShortKitPlayerNativeView.swift +1 -1
- package/ios/ShortKitSDK.xcframework/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +417 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +27739 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +790 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +790 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +417 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +27739 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +790 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +790 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitWidgetNativeView.swift +1 -1
- package/package.json +1 -1
- package/src/CarouselOverlayManager.tsx +71 -0
- package/src/ShortKitContext.ts +13 -0
- package/src/ShortKitFeed.tsx +3 -0
- package/src/ShortKitProvider.tsx +118 -2
- package/src/index.ts +3 -1
- package/src/serialization.ts +6 -2
- package/src/specs/NativeShortKitModule.ts +19 -0
- package/src/types.ts +4 -3
- package/src/useShortKitCarousel.ts +29 -0
- 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
|
-
|
|
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
|
-
/**
|
|
54
|
+
/** Video overlay for the currently active cell (nativeID="overlay-current"). */
|
|
55
55
|
private var currentOverlayView: View? = null
|
|
56
56
|
|
|
57
|
-
/**
|
|
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
|
|
238
|
+
// Current overlays follow the active cell
|
|
224
239
|
currentOverlayView?.translationY = -delta
|
|
240
|
+
currentCarouselOverlayView?.translationY = -delta
|
|
225
241
|
|
|
226
|
-
// Next
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
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
|
|
243
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Combine
|
|
3
|
-
import
|
|
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:
|
|
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
|
|
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
|
-
///
|
|
30
|
+
/// Video overlay for the currently active cell (nativeID="overlay-current").
|
|
31
31
|
private weak var currentOverlayView: UIView?
|
|
32
|
-
///
|
|
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
|
|
173
|
-
currentOverlayView?.transform =
|
|
187
|
+
// Current overlays follow the active cell
|
|
188
|
+
currentOverlayView?.transform = translateY
|
|
189
|
+
currentCarouselOverlayView?.transform = translateY
|
|
174
190
|
|
|
175
|
-
// Next
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|
package/ios/ShortKitModule.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|