@shortkitsdk/react-native 0.2.11 → 0.2.12
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/android/build.gradle.kts +13 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +115 -55
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +67 -56
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +71 -26
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +160 -35
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +5 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +43 -10
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +9 -0
- package/ios/ReactOverlayHost.swift +13 -27
- package/ios/ShortKitBridge.swift +36 -2
- package/ios/ShortKitFeedView.swift +24 -3
- package/ios/ShortKitModule.mm +4 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +720 -144
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +19 -5
- 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 +19 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +720 -144
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +19 -5
- 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 +19 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/package.json +1 -1
- package/src/ShortKitContext.ts +2 -1
- package/src/ShortKitFeed.tsx +14 -0
- package/src/ShortKitOverlaySurface.tsx +153 -45
- package/src/ShortKitPlayer.tsx +25 -3
- package/src/ShortKitProvider.tsx +4 -2
- package/src/index.ts +4 -0
- package/src/serialization.ts +1 -0
- package/src/specs/NativeShortKitModule.ts +18 -1
- package/src/types.ts +6 -0
|
@@ -24,8 +24,10 @@ import com.shortkit.sdk.config.CarouselOverlayMode
|
|
|
24
24
|
import com.shortkit.sdk.config.FeedConfig
|
|
25
25
|
import com.shortkit.sdk.config.FeedHeight
|
|
26
26
|
import com.shortkit.sdk.config.FeedSource
|
|
27
|
+
import com.shortkit.sdk.config.ScrollAxis
|
|
27
28
|
import com.shortkit.sdk.config.SurveyOverlayMode
|
|
28
29
|
import com.shortkit.sdk.config.VideoOverlayMode
|
|
30
|
+
import com.shortkit.sdk.feed.FeedPreload
|
|
29
31
|
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
30
32
|
import kotlinx.coroutines.CoroutineScope
|
|
31
33
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -74,6 +76,7 @@ class ShortKitBridge(
|
|
|
74
76
|
return try {
|
|
75
77
|
val obj = JSONObject().apply {
|
|
76
78
|
put("id", item.id)
|
|
79
|
+
item.playbackId?.let { put("playbackId", it) }
|
|
77
80
|
put("title", item.title)
|
|
78
81
|
item.description?.let { put("description", it) }
|
|
79
82
|
put("duration", item.duration)
|
|
@@ -138,17 +141,23 @@ class ShortKitBridge(
|
|
|
138
141
|
return try {
|
|
139
142
|
val obj = JSONObject(json)
|
|
140
143
|
|
|
144
|
+
val overlayRaw = obj.optString("overlay", null)
|
|
145
|
+
|
|
141
146
|
val feedHeight = parseFeedHeight(obj.optString("feedHeight", null))
|
|
142
147
|
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
143
|
-
val videoOverlay = parseVideoOverlay(
|
|
148
|
+
val videoOverlay = parseVideoOverlay(overlayRaw, context)
|
|
144
149
|
|
|
145
150
|
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
146
151
|
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
147
152
|
|
|
148
|
-
val
|
|
153
|
+
val carouselOverlayRaw = obj.optString("carouselOverlay", null)
|
|
154
|
+
val carouselOverlay = parseCarouselOverlay(carouselOverlayRaw, context)
|
|
149
155
|
val autoplay = obj.optBoolean("autoplay", true)
|
|
150
156
|
val filter = obj.optJSONObject("filter")?.let { parseFeedFilterToModel(it.toString()) }
|
|
151
157
|
|
|
158
|
+
val scrollAxisStr = obj.optString("scrollAxis", "vertical")
|
|
159
|
+
val scrollAxis = if (scrollAxisStr == "horizontal") ScrollAxis.Horizontal else ScrollAxis.Vertical
|
|
160
|
+
|
|
152
161
|
FeedConfig(
|
|
153
162
|
feedHeight = feedHeight,
|
|
154
163
|
videoOverlay = videoOverlay,
|
|
@@ -159,8 +168,10 @@ class ShortKitBridge(
|
|
|
159
168
|
autoplay = autoplay,
|
|
160
169
|
feedSource = feedSource,
|
|
161
170
|
filter = filter,
|
|
171
|
+
scrollAxis = scrollAxis,
|
|
162
172
|
)
|
|
163
|
-
} catch (
|
|
173
|
+
} catch (e: Exception) {
|
|
174
|
+
android.util.Log.e("SK:Bridge", "parseFeedConfig: EXCEPTION parsing config, returning default. json=${json.take(300)}", e)
|
|
164
175
|
FeedConfig()
|
|
165
176
|
}
|
|
166
177
|
}
|
|
@@ -191,7 +202,9 @@ class ShortKitBridge(
|
|
|
191
202
|
* - `"{\"type\":\"custom\"}"` -> Custom with ReactOverlayHost factory
|
|
192
203
|
*/
|
|
193
204
|
private fun parseVideoOverlay(json: String?, context: android.content.Context?): VideoOverlayMode {
|
|
194
|
-
if (json.isNullOrEmpty())
|
|
205
|
+
if (json.isNullOrEmpty()) {
|
|
206
|
+
return VideoOverlayMode.None
|
|
207
|
+
}
|
|
195
208
|
return try {
|
|
196
209
|
val parsed = json.trim()
|
|
197
210
|
|
|
@@ -205,14 +218,18 @@ class ShortKitBridge(
|
|
|
205
218
|
JSONObject(parsed)
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
val type = inner.optString("type")
|
|
222
|
+
val name = inner.optString("name", "Default")
|
|
223
|
+
|
|
224
|
+
if (type == "custom" && context != null) {
|
|
210
225
|
val ctx = context.applicationContext
|
|
211
226
|
VideoOverlayMode.Custom { ReactOverlayHost(ctx).apply { overlayName = name } }
|
|
212
227
|
} else {
|
|
228
|
+
android.util.Log.w("SK:Bridge", "parseVideoOverlay: → None (type='$type' context=${if (context != null) "OK" else "NULL"})")
|
|
213
229
|
VideoOverlayMode.None
|
|
214
230
|
}
|
|
215
|
-
} catch (
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
android.util.Log.e("SK:Bridge", "parseVideoOverlay: EXCEPTION → None. json=${json.take(200)}", e)
|
|
216
233
|
VideoOverlayMode.None
|
|
217
234
|
}
|
|
218
235
|
}
|
|
@@ -378,7 +395,7 @@ class ShortKitBridge(
|
|
|
378
395
|
private var scope: CoroutineScope? = null
|
|
379
396
|
|
|
380
397
|
/** Preload handles keyed by UUID, consumed by feed views via preloadId prop. */
|
|
381
|
-
var preloadHandles = mutableMapOf<String,
|
|
398
|
+
var preloadHandles = mutableMapOf<String, FeedPreload>()
|
|
382
399
|
private set
|
|
383
400
|
|
|
384
401
|
// ------------------------------------------------------------------
|
|
@@ -450,8 +467,11 @@ class ShortKitBridge(
|
|
|
450
467
|
pending = staticPendingFeedViews.toList()
|
|
451
468
|
staticPendingFeedViews.clear()
|
|
452
469
|
}
|
|
470
|
+
val mainHandler = Handler(Looper.getMainLooper())
|
|
453
471
|
for (view in pending) {
|
|
454
|
-
|
|
472
|
+
mainHandler.post {
|
|
473
|
+
if (view.isAttachedToWindow) view.embedFeedFragmentIfNeeded()
|
|
474
|
+
}
|
|
455
475
|
}
|
|
456
476
|
}
|
|
457
477
|
|
|
@@ -490,19 +510,29 @@ class ShortKitBridge(
|
|
|
490
510
|
}
|
|
491
511
|
emitEventOnMain("onRemainingContentCountChanged", params)
|
|
492
512
|
}
|
|
513
|
+
fragment.onFeedReady = {
|
|
514
|
+
val params = Arguments.createMap().apply {
|
|
515
|
+
putString("feedId", id)
|
|
516
|
+
}
|
|
517
|
+
emitEventOnMain("onFeedReady", params)
|
|
518
|
+
}
|
|
493
519
|
|
|
494
|
-
// Replay buffered operations
|
|
520
|
+
// Replay buffered operations after the fragment's view is created.
|
|
521
|
+
// commitNowAllowingStateLoss triggers onCreate but onViewCreated
|
|
522
|
+
// (which sets up customFeedController) runs on a later main-thread
|
|
523
|
+
// tick. A single post isn't enough — use postDelayed to ensure the
|
|
524
|
+
// fragment's view lifecycle has completed.
|
|
495
525
|
val ops: List<(ShortKitFeedFragment) -> Unit>?
|
|
496
526
|
synchronized(pendingOpsLock) {
|
|
497
527
|
ops = pendingOps.remove(id)?.toList()
|
|
498
528
|
}
|
|
499
529
|
if (ops != null) {
|
|
500
|
-
Handler(Looper.getMainLooper()).
|
|
501
|
-
val frag = feedRegistry[id]?.get() ?: return@
|
|
530
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
531
|
+
val frag = feedRegistry[id]?.get() ?: return@postDelayed
|
|
502
532
|
for (op in ops) {
|
|
503
533
|
op(frag)
|
|
504
534
|
}
|
|
505
|
-
}
|
|
535
|
+
}, 100)
|
|
506
536
|
}
|
|
507
537
|
}
|
|
508
538
|
|
|
@@ -544,7 +574,7 @@ class ShortKitBridge(
|
|
|
544
574
|
// Preload handle management
|
|
545
575
|
// ------------------------------------------------------------------
|
|
546
576
|
|
|
547
|
-
fun consumePreload(id: String):
|
|
577
|
+
fun consumePreload(id: String): FeedPreload? {
|
|
548
578
|
return preloadHandles.remove(id)
|
|
549
579
|
}
|
|
550
580
|
|
|
@@ -714,13 +744,37 @@ class ShortKitBridge(
|
|
|
714
744
|
// Preload feed
|
|
715
745
|
// ------------------------------------------------------------------
|
|
716
746
|
|
|
717
|
-
fun preloadFeed(configJSON: String, callback: (String) -> Unit) {
|
|
747
|
+
fun preloadFeed(configJSON: String, itemsJSON: String?, callback: (String) -> Unit) {
|
|
718
748
|
val sdk = shortKit
|
|
719
749
|
if (sdk == null) {
|
|
720
750
|
callback("")
|
|
721
751
|
return
|
|
722
752
|
}
|
|
723
|
-
val
|
|
753
|
+
val config = parseFeedConfig(configJSON)
|
|
754
|
+
if (config.feedSource == FeedSource.CUSTOM) {
|
|
755
|
+
if (itemsJSON == null) {
|
|
756
|
+
android.util.Log.w("ShortKit", "preloadFeed called with feedSource=CUSTOM but no items")
|
|
757
|
+
callback("")
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
val items = parseFeedInputs(itemsJSON)
|
|
761
|
+
if (items == null) {
|
|
762
|
+
callback("")
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
val preload = sdk.preloadFeed(items = items)
|
|
766
|
+
val uuid = java.util.UUID.randomUUID().toString()
|
|
767
|
+
preloadHandles[uuid] = preload
|
|
768
|
+
callback(uuid)
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
val filterJSON = config.filter?.let { org.json.JSONObject().apply {
|
|
772
|
+
it.tags?.let { tags -> put("tags", org.json.JSONArray(tags)) }
|
|
773
|
+
it.section?.let { s -> put("section", s) }
|
|
774
|
+
it.author?.let { a -> put("author", a) }
|
|
775
|
+
it.contentType?.let { ct -> put("contentType", ct) }
|
|
776
|
+
}.toString() }
|
|
777
|
+
val preload = sdk.preloadFeed(filter = filterJSON)
|
|
724
778
|
val uuid = java.util.UUID.randomUUID().toString()
|
|
725
779
|
preloadHandles[uuid] = preload
|
|
726
780
|
callback(uuid)
|
|
@@ -952,16 +1006,7 @@ class ShortKitBridge(
|
|
|
952
1006
|
}
|
|
953
1007
|
}
|
|
954
1008
|
|
|
955
|
-
// Remaining content count
|
|
956
|
-
newScope.launch {
|
|
957
|
-
player.remainingContentCount.collect { count ->
|
|
958
|
-
val params = Arguments.createMap().apply {
|
|
959
|
-
putString("feedId", "") // Global fallback — per-feed routing via fragment callback
|
|
960
|
-
putInt("count", count)
|
|
961
|
-
}
|
|
962
|
-
emitEvent.invoke("onRemainingContentCountChanged", params)
|
|
963
|
-
}
|
|
964
|
-
}
|
|
1009
|
+
// Remaining content count — handled per-feed via fragment.onRemainingContentCountChange callback
|
|
965
1010
|
|
|
966
1011
|
// Feed scroll phase
|
|
967
1012
|
newScope.launch {
|
|
@@ -9,7 +9,6 @@ import android.view.View
|
|
|
9
9
|
import android.view.ViewGroup
|
|
10
10
|
import android.widget.FrameLayout
|
|
11
11
|
import androidx.fragment.app.FragmentActivity
|
|
12
|
-
import androidx.viewpager2.widget.ViewPager2
|
|
13
12
|
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -41,6 +40,11 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
41
40
|
private var fragmentContainerId: Int = View.generateViewId()
|
|
42
41
|
private val handler = Handler(Looper.getMainLooper())
|
|
43
42
|
|
|
43
|
+
// The React Native-assigned size. Stored so we can re-apply it after the
|
|
44
|
+
// SDK's forceLayoutIfNeeded potentially re-measures to full-screen size.
|
|
45
|
+
private var rnWidth: Int = 0
|
|
46
|
+
private var rnHeight: Int = 0
|
|
47
|
+
|
|
44
48
|
init {
|
|
45
49
|
// Use a child FrameLayout as the fragment container. Fabric overrides
|
|
46
50
|
// this view's own id, so we can't use it as the fragment container.
|
|
@@ -73,6 +77,42 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
73
77
|
layout(left, top, right, bottom)
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Re-apply the React Native-assigned size to the fragment container and
|
|
82
|
+
* all descendants (fragment root, VP, RV). The SDK's forceLayoutIfNeeded
|
|
83
|
+
* may have sized them to the full activity height instead.
|
|
84
|
+
*/
|
|
85
|
+
private val constrainRunnable = Runnable {
|
|
86
|
+
val w = rnWidth
|
|
87
|
+
val h = rnHeight
|
|
88
|
+
if (w == 0 || h == 0) return@Runnable
|
|
89
|
+
|
|
90
|
+
val fragView = feedFragment?.view ?: return@Runnable
|
|
91
|
+
val wSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY)
|
|
92
|
+
val hSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
|
|
93
|
+
|
|
94
|
+
// Re-pin container
|
|
95
|
+
fragmentContainer.measure(wSpec, hSpec)
|
|
96
|
+
fragmentContainer.layout(0, 0, w, h)
|
|
97
|
+
|
|
98
|
+
// Re-pin fragment root, VP, and RV if they got the wrong size
|
|
99
|
+
if (fragView.height != h) {
|
|
100
|
+
fragView.measure(wSpec, hSpec)
|
|
101
|
+
fragView.layout(0, 0, w, h)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val vp = findViewPager2(fragView)
|
|
105
|
+
if (vp != null && vp.height != h) {
|
|
106
|
+
vp.measure(wSpec, hSpec)
|
|
107
|
+
vp.layout(0, 0, w, h)
|
|
108
|
+
val rv = vp.getChildAt(0) as? ViewGroup
|
|
109
|
+
if (rv != null) {
|
|
110
|
+
rv.measure(wSpec, hSpec)
|
|
111
|
+
rv.layout(0, 0, w, h)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
|
|
77
117
|
// -----------------------------------------------------------------------
|
|
78
118
|
// Lifecycle
|
|
@@ -80,14 +120,21 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
80
120
|
|
|
81
121
|
override fun onAttachedToWindow() {
|
|
82
122
|
super.onAttachedToWindow()
|
|
83
|
-
|
|
123
|
+
// Defer to the next message-loop tick. onAttachedToWindow can fire
|
|
124
|
+
// during a parent fragment transaction (e.g. react-native-screens
|
|
125
|
+
// ScreenStack.onUpdate), and FragmentManager forbids nested
|
|
126
|
+
// commitNow calls. Posting ensures we run after the outer
|
|
127
|
+
// transaction completes — still before the first draw pass.
|
|
128
|
+
handler.post {
|
|
129
|
+
if (isAttachedToWindow) embedFeedFragmentIfNeeded()
|
|
130
|
+
}
|
|
84
131
|
}
|
|
85
132
|
|
|
86
133
|
override fun onDetachedFromWindow() {
|
|
87
134
|
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
88
135
|
ShortKitBridge.staticPendingFeedViews.remove(this)
|
|
89
136
|
}
|
|
90
|
-
|
|
137
|
+
suspendFeedFragment()
|
|
91
138
|
super.onDetachedFromWindow()
|
|
92
139
|
}
|
|
93
140
|
|
|
@@ -96,7 +143,35 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
96
143
|
// -----------------------------------------------------------------------
|
|
97
144
|
|
|
98
145
|
internal fun embedFeedFragmentIfNeeded() {
|
|
99
|
-
|
|
146
|
+
val activity = getReactActivity()
|
|
147
|
+
if (activity == null) {
|
|
148
|
+
Log.e(TAG, "embedFeedFragment: FragmentActivity not found in context chain")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Show a hidden fragment (e.g. returning to this tab).
|
|
153
|
+
// Using hide()/show() instead of detach()/attach() preserves the
|
|
154
|
+
// entire view hierarchy (ViewPager, RecyclerView, player surface)
|
|
155
|
+
// so there is zero flicker — matching iOS behavior exactly.
|
|
156
|
+
val existingFragment = feedFragment
|
|
157
|
+
if (existingFragment != null) {
|
|
158
|
+
if (existingFragment.isHidden) {
|
|
159
|
+
try {
|
|
160
|
+
activity.supportFragmentManager
|
|
161
|
+
.beginTransaction()
|
|
162
|
+
.show(existingFragment)
|
|
163
|
+
.commitNowAllowingStateLoss()
|
|
164
|
+
existingFragment.activate()
|
|
165
|
+
} catch (e: Exception) {
|
|
166
|
+
Log.e(TAG, "Failed to show feed fragment", e)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
feedId?.let { id ->
|
|
170
|
+
ShortKitBridge.shared?.registerFeed(id)
|
|
171
|
+
ShortKitBridge.shared?.registerFeedFragment(id, existingFragment)
|
|
172
|
+
}
|
|
173
|
+
return
|
|
174
|
+
}
|
|
100
175
|
|
|
101
176
|
val sdk = ShortKitBridge.shared?.sdk ?: run {
|
|
102
177
|
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
@@ -105,15 +180,16 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
105
180
|
return
|
|
106
181
|
}
|
|
107
182
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
val feedConfig = ShortKitBridge.parseFeedConfig(config ?: "{}", context)
|
|
183
|
+
var feedConfig = ShortKitBridge.parseFeedConfig(config ?: "{}", context)
|
|
111
184
|
|
|
112
185
|
preloadId?.let { id ->
|
|
113
|
-
ShortKitBridge.shared?.consumePreload(id)
|
|
186
|
+
val preload = ShortKitBridge.shared?.consumePreload(id)
|
|
187
|
+
if (preload != null) {
|
|
188
|
+
feedConfig = feedConfig.copy(preload = preload)
|
|
189
|
+
}
|
|
114
190
|
}
|
|
115
191
|
|
|
116
|
-
val fragment = ShortKitFeedFragment.newInstance(sdk, feedConfig)
|
|
192
|
+
val fragment = ShortKitFeedFragment.newInstance(sdk, feedConfig, startAtItemId)
|
|
117
193
|
|
|
118
194
|
try {
|
|
119
195
|
activity.supportFragmentManager
|
|
@@ -122,34 +198,39 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
122
198
|
.commitNowAllowingStateLoss()
|
|
123
199
|
this.feedFragment = fragment
|
|
124
200
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
201
|
+
// View.generateViewId() can return IDs that collide with Fabric-
|
|
202
|
+
// managed views (e.g. ReactSurfaceView also gets id=1). When that
|
|
203
|
+
// happens, FragmentManager.replace() places the fragment view in
|
|
204
|
+
// the wrong parent. Detect and correct this by reparenting.
|
|
205
|
+
val fragView = fragment.view
|
|
206
|
+
val fragParent = fragView?.parent
|
|
207
|
+
if (fragParent != null && fragParent !== fragmentContainer) {
|
|
208
|
+
Log.w(TAG, "embedFeedFragment: reparenting — fragment placed in ${fragParent.javaClass.simpleName} due to ID collision")
|
|
209
|
+
(fragParent as? ViewGroup)?.removeView(fragView)
|
|
210
|
+
fragmentContainer.addView(fragView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Pin the fragment container to the React Native-assigned size.
|
|
214
|
+
// Without this, MATCH_PARENT causes Android to re-measure the
|
|
215
|
+
// container to the full activity size, and the SDK's deferred
|
|
216
|
+
// layout forcing picks up the wrong (full-screen) dimensions.
|
|
217
|
+
rnWidth = measuredWidth
|
|
218
|
+
rnHeight = measuredHeight
|
|
219
|
+
fragmentContainer.layoutParams = LayoutParams(measuredWidth, measuredHeight)
|
|
127
220
|
fragmentContainer.measure(
|
|
128
221
|
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
129
222
|
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
|
130
223
|
)
|
|
131
224
|
fragmentContainer.layout(0, 0, measuredWidth, measuredHeight)
|
|
225
|
+
// Schedule a delayed re-pin: the SDK's forceLayoutIfNeeded will
|
|
226
|
+
// run when deferCellSetup exhausts retries (~3 post ticks later)
|
|
227
|
+
// and may size the VP/RV to the wrong dimensions. Re-apply our
|
|
228
|
+
// correct RN size after it runs.
|
|
229
|
+
handler.postDelayed(constrainRunnable, 500)
|
|
132
230
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// next frame. Schedule a delayed nudge that runs after layout settles.
|
|
137
|
-
handler.postDelayed({
|
|
138
|
-
val fragView = feedFragment?.view
|
|
139
|
-
val vp = if (fragView != null) findViewPager2(fragView) else findViewPager2(fragmentContainer)
|
|
140
|
-
if (vp != null) {
|
|
141
|
-
if (vp.width == 0 || vp.height == 0) {
|
|
142
|
-
fragView?.measure(
|
|
143
|
-
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
144
|
-
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
|
145
|
-
)
|
|
146
|
-
fragView?.layout(0, 0, measuredWidth, measuredHeight)
|
|
147
|
-
}
|
|
148
|
-
if (vp.adapter != null && vp.adapter!!.itemCount > 0) {
|
|
149
|
-
vp.setCurrentItem(vp.currentItem, false)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}, 200)
|
|
231
|
+
if (measuredWidth == 0 || measuredHeight == 0) {
|
|
232
|
+
Log.w(TAG, "embedFeedFragment: WARNING — container is 0x0, overlays will be invisible. Check if parent has display:none or flex:0")
|
|
233
|
+
}
|
|
153
234
|
|
|
154
235
|
feedId?.let { id ->
|
|
155
236
|
ShortKitBridge.shared?.registerFeed(id)
|
|
@@ -160,12 +241,56 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
160
241
|
}
|
|
161
242
|
}
|
|
162
243
|
|
|
163
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Hide the fragment without destroying its view hierarchy (mirrors iOS
|
|
246
|
+
* `suspendFeedViewController`). Using hide() instead of detach() keeps
|
|
247
|
+
* the ViewPager, RecyclerView, player surface, and all overlay views
|
|
248
|
+
* alive in memory — so returning to the tab is instant with no flicker.
|
|
249
|
+
*/
|
|
250
|
+
private fun suspendFeedFragment() {
|
|
251
|
+
feedId?.let { id ->
|
|
252
|
+
ShortKitBridge.shared?.unregisterFeed(id)
|
|
253
|
+
ShortKitBridge.shared?.unregisterFeedFragment(id)
|
|
254
|
+
}
|
|
255
|
+
val fragment = feedFragment ?: return
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
fragment.deactivate()
|
|
259
|
+
val activity = getReactActivity() ?: return
|
|
260
|
+
activity.supportFragmentManager
|
|
261
|
+
.beginTransaction()
|
|
262
|
+
.hide(fragment)
|
|
263
|
+
.commitNowAllowingStateLoss()
|
|
264
|
+
} catch (e: IllegalStateException) {
|
|
265
|
+
// Expected during Activity teardown — fall back to full destroy
|
|
266
|
+
feedFragment = null
|
|
267
|
+
} catch (e: Exception) {
|
|
268
|
+
Log.e(TAG, "Unexpected error suspending feed fragment", e)
|
|
269
|
+
feedFragment = null
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Called by [ShortKitFeedViewManager.onDropViewInstance] when React
|
|
275
|
+
* unmounts the native view. Performs a full fragment teardown.
|
|
276
|
+
*/
|
|
277
|
+
fun destroy() {
|
|
278
|
+
handler.removeCallbacks(constrainRunnable)
|
|
279
|
+
handler.removeCallbacks(layoutRunnable)
|
|
280
|
+
destroyFeedFragment()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Full teardown — removes the fragment from FragmentManager entirely.
|
|
285
|
+
* Called when the Fabric view is being destroyed (not just hidden).
|
|
286
|
+
*/
|
|
287
|
+
private fun destroyFeedFragment() {
|
|
164
288
|
feedId?.let { id ->
|
|
165
289
|
ShortKitBridge.shared?.unregisterFeed(id)
|
|
166
290
|
ShortKitBridge.shared?.unregisterFeedFragment(id)
|
|
167
291
|
}
|
|
168
292
|
val fragment = feedFragment ?: return
|
|
293
|
+
fragment.deactivate()
|
|
169
294
|
feedFragment = null
|
|
170
295
|
|
|
171
296
|
try {
|
|
@@ -185,9 +310,9 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
185
310
|
// Helpers
|
|
186
311
|
// -----------------------------------------------------------------------
|
|
187
312
|
|
|
188
|
-
/** Recursively find the first
|
|
189
|
-
private fun findViewPager2(view: View): ViewPager2? {
|
|
190
|
-
if (view is ViewPager2) return view
|
|
313
|
+
/** Recursively find the first ViewPager2 in a view hierarchy. */
|
|
314
|
+
private fun findViewPager2(view: View): androidx.viewpager2.widget.ViewPager2? {
|
|
315
|
+
if (view is androidx.viewpager2.widget.ViewPager2) return view
|
|
191
316
|
if (view is ViewGroup) {
|
|
192
317
|
for (i in 0 until view.childCount) {
|
|
193
318
|
findViewPager2(view.getChildAt(i))?.let { return it }
|
|
@@ -35,6 +35,11 @@ class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
|
|
|
35
35
|
view.preloadId = preloadId
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
override fun onDropViewInstance(view: ShortKitFeedView) {
|
|
39
|
+
view.destroy()
|
|
40
|
+
super.onDropViewInstance(view)
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
companion object {
|
|
39
44
|
const val REACT_CLASS = "ShortKitFeedView"
|
|
40
45
|
}
|
|
@@ -205,13 +205,13 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
@ReactMethod
|
|
208
|
-
override fun preloadFeed(configJSON: String, promise: Promise) {
|
|
208
|
+
override fun preloadFeed(configJSON: String, itemsJSON: String?, promise: Promise) {
|
|
209
209
|
val b = bridge
|
|
210
210
|
if (b == null) {
|
|
211
211
|
promise.resolve("")
|
|
212
212
|
return
|
|
213
213
|
}
|
|
214
|
-
b.preloadFeed(configJSON) { result -> promise.resolve(result) }
|
|
214
|
+
b.preloadFeed(configJSON, itemsJSON) { result -> promise.resolve(result) }
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
// -----------------------------------------------------------------
|
|
@@ -238,15 +238,48 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
238
238
|
// -----------------------------------------------------------------
|
|
239
239
|
|
|
240
240
|
private fun sendEvent(name: String, params: WritableMap) {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// is torn down.
|
|
241
|
+
// Use the codegen-generated emitOn* methods which route through
|
|
242
|
+
// mEventEmitterCallback (JSI direct channel). The legacy
|
|
243
|
+
// DeviceEventManagerModule.RCTDeviceEventEmitter path does NOT
|
|
244
|
+
// reach codegen EventEmitter<T> subscribers in RN 0.78 new arch.
|
|
246
245
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
when (name) {
|
|
247
|
+
"onPlayerStateChanged" -> emitOnPlayerStateChanged(params)
|
|
248
|
+
"onCurrentItemChanged" -> emitOnCurrentItemChanged(params)
|
|
249
|
+
"onTimeUpdate" -> emitOnTimeUpdate(params)
|
|
250
|
+
"onMutedChanged" -> emitOnMutedChanged(params)
|
|
251
|
+
"onPlaybackRateChanged" -> emitOnPlaybackRateChanged(params)
|
|
252
|
+
"onCaptionsEnabledChanged" -> emitOnCaptionsEnabledChanged(params)
|
|
253
|
+
"onActiveCaptionTrackChanged" -> emitOnActiveCaptionTrackChanged(params)
|
|
254
|
+
"onActiveCueChanged" -> emitOnActiveCueChanged(params)
|
|
255
|
+
"onDidLoop" -> emitOnDidLoop(params)
|
|
256
|
+
"onFeedTransition" -> emitOnFeedTransition(params)
|
|
257
|
+
"onFeedScrollPhase" -> emitOnFeedScrollPhase(params)
|
|
258
|
+
"onFormatChange" -> emitOnFormatChange(params)
|
|
259
|
+
"onPrefetchedAheadCountChanged" -> emitOnPrefetchedAheadCountChanged(params)
|
|
260
|
+
"onRemainingContentCountChanged" -> emitOnRemainingContentCountChanged(params)
|
|
261
|
+
"onSurveyResponse" -> emitOnSurveyResponse(params)
|
|
262
|
+
"onContentTapped" -> emitOnContentTapped(params)
|
|
263
|
+
"onDismiss" -> emitOnDismiss(params)
|
|
264
|
+
"onRefreshRequested" -> emitOnRefreshRequested(params)
|
|
265
|
+
"onDidFetchContentItems" -> emitOnDidFetchContentItems(params)
|
|
266
|
+
"onFeedReady" -> emitOnFeedReady(params)
|
|
267
|
+
"onOverlayActiveChanged" -> emitOnOverlayActiveChanged(params)
|
|
268
|
+
"onOverlayPlayerStateChanged" -> emitOnOverlayPlayerStateChanged(params)
|
|
269
|
+
"onOverlayMutedChanged" -> emitOnOverlayMutedChanged(params)
|
|
270
|
+
"onOverlayPlaybackRateChanged" -> emitOnOverlayPlaybackRateChanged(params)
|
|
271
|
+
"onOverlayCaptionsEnabledChanged" -> emitOnOverlayCaptionsEnabledChanged(params)
|
|
272
|
+
"onOverlayActiveCueChanged" -> emitOnOverlayActiveCueChanged(params)
|
|
273
|
+
"onOverlayFeedScrollPhaseChanged" -> emitOnOverlayFeedScrollPhaseChanged(params)
|
|
274
|
+
"onOverlayTimeUpdate" -> emitOnOverlayTimeUpdate(params)
|
|
275
|
+
"onOverlayFullState" -> emitOnOverlayFullState(params)
|
|
276
|
+
else -> {
|
|
277
|
+
android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
|
|
278
|
+
reactApplicationContext
|
|
279
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
280
|
+
.emit(name, params)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
250
283
|
} catch (e: Exception) {
|
|
251
284
|
android.util.Log.e("SK:Module", "sendEvent($name) EXCEPTION: ${e.message}", e)
|
|
252
285
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.shortkit.reactnative
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.view.MotionEvent
|
|
4
5
|
import android.widget.FrameLayout
|
|
5
6
|
import com.shortkit.sdk.config.PlayerClickAction
|
|
6
7
|
import com.shortkit.sdk.config.PlayerConfig
|
|
@@ -42,6 +43,14 @@ class ShortKitPlayerNativeView(context: Context) : FrameLayout(context) {
|
|
|
42
43
|
applyActive()
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
47
|
+
return super.onInterceptTouchEvent(ev)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
|
51
|
+
return super.dispatchTouchEvent(ev)
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
override fun onAttachedToWindow() {
|
|
46
55
|
super.onAttachedToWindow()
|
|
47
56
|
rebuildIfNeeded()
|
|
@@ -165,41 +165,27 @@ import ShortKitSDK
|
|
|
165
165
|
/// Emit all cached state as individual events. Called from activatePlayback()
|
|
166
166
|
/// (deferred) and can be called whenever we need to synchronize JS state.
|
|
167
167
|
private func emitFullState() {
|
|
168
|
-
|
|
169
|
-
"surfaceId": surfaceId,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
])
|
|
177
|
-
bridge?.emit("onOverlayPlaybackRateChanged", body: [
|
|
178
|
-
"surfaceId": surfaceId, "playbackRate": cachedPlaybackRate
|
|
179
|
-
])
|
|
180
|
-
bridge?.emit("onOverlayCaptionsEnabledChanged", body: [
|
|
181
|
-
"surfaceId": surfaceId, "captionsEnabled": cachedCaptionsEnabled
|
|
182
|
-
])
|
|
168
|
+
var body: [String: Any] = [
|
|
169
|
+
"surfaceId": surfaceId,
|
|
170
|
+
"isActive": true,
|
|
171
|
+
"playerState": cachedPlayerState,
|
|
172
|
+
"isMuted": cachedIsMuted,
|
|
173
|
+
"playbackRate": cachedPlaybackRate,
|
|
174
|
+
"captionsEnabled": cachedCaptionsEnabled,
|
|
175
|
+
]
|
|
183
176
|
if let cue = cachedActiveCue,
|
|
184
177
|
let cueData = try? JSONSerialization.data(withJSONObject: cue),
|
|
185
178
|
let cueJson = String(data: cueData, encoding: .utf8) {
|
|
186
|
-
|
|
187
|
-
"surfaceId": surfaceId, "activeCue": cueJson
|
|
188
|
-
])
|
|
179
|
+
body["activeCue"] = cueJson
|
|
189
180
|
} else {
|
|
190
|
-
|
|
191
|
-
"surfaceId": surfaceId, "activeCue": NSNull()
|
|
192
|
-
])
|
|
181
|
+
body["activeCue"] = NSNull()
|
|
193
182
|
}
|
|
194
183
|
if let scrollPhase = cachedFeedScrollPhase {
|
|
195
|
-
|
|
196
|
-
"surfaceId": surfaceId, "feedScrollPhase": scrollPhase
|
|
197
|
-
])
|
|
184
|
+
body["feedScrollPhase"] = scrollPhase
|
|
198
185
|
} else {
|
|
199
|
-
|
|
200
|
-
"surfaceId": surfaceId, "feedScrollPhase": NSNull()
|
|
201
|
-
])
|
|
186
|
+
body["feedScrollPhase"] = NSNull()
|
|
202
187
|
}
|
|
188
|
+
bridge?.emit("onOverlayFullState", body: body)
|
|
203
189
|
}
|
|
204
190
|
|
|
205
191
|
// MARK: - Surface Creation
|