@shortkitsdk/react-native 0.2.11 → 0.2.14
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 +157 -54
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +67 -56
- package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +154 -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 +45 -10
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +9 -0
- package/ios/ReactCarouselOverlayHost.swift +37 -17
- package/ios/ReactOverlayHost.swift +33 -35
- package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
- package/ios/ShortKitBridge.swift +78 -2
- package/ios/ShortKitFeedView.swift +24 -3
- package/ios/ShortKitModule.mm +6 -2
- package/ios/ShortKitSDK.xcframework/Info.plist +4 -4
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +69 -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 +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +69 -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 +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework.bak2/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
- package/src/ShortKitContext.ts +2 -1
- package/src/ShortKitFeed.tsx +19 -1
- package/src/ShortKitOverlaySurface.tsx +148 -41
- package/src/ShortKitPlayer.tsx +25 -3
- package/src/ShortKitProvider.tsx +4 -2
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
- package/src/index.ts +8 -1
- package/src/serialization.ts +8 -0
- package/src/specs/NativeShortKitModule.ts +31 -1
- package/src/types.ts +45 -1
|
@@ -17,6 +17,7 @@ import com.shortkit.sdk.model.FeedInput
|
|
|
17
17
|
import com.shortkit.sdk.model.FeedScrollPhase
|
|
18
18
|
import com.shortkit.sdk.model.FeedTransitionPhase
|
|
19
19
|
import com.shortkit.sdk.model.ImageCarouselItem
|
|
20
|
+
import com.shortkit.sdk.model.VideoCarouselItem
|
|
20
21
|
import com.shortkit.sdk.model.JsonValue
|
|
21
22
|
import com.shortkit.sdk.model.PlayerState
|
|
22
23
|
import com.shortkit.sdk.config.AdOverlayMode
|
|
@@ -24,8 +25,11 @@ import com.shortkit.sdk.config.CarouselOverlayMode
|
|
|
24
25
|
import com.shortkit.sdk.config.FeedConfig
|
|
25
26
|
import com.shortkit.sdk.config.FeedHeight
|
|
26
27
|
import com.shortkit.sdk.config.FeedSource
|
|
28
|
+
import com.shortkit.sdk.config.ScrollAxis
|
|
27
29
|
import com.shortkit.sdk.config.SurveyOverlayMode
|
|
30
|
+
import com.shortkit.sdk.config.VideoCarouselOverlayMode
|
|
28
31
|
import com.shortkit.sdk.config.VideoOverlayMode
|
|
32
|
+
import com.shortkit.sdk.feed.FeedPreload
|
|
29
33
|
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
30
34
|
import kotlinx.coroutines.CoroutineScope
|
|
31
35
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -74,6 +78,7 @@ class ShortKitBridge(
|
|
|
74
78
|
return try {
|
|
75
79
|
val obj = JSONObject().apply {
|
|
76
80
|
put("id", item.id)
|
|
81
|
+
item.playbackId?.let { put("playbackId", it) }
|
|
77
82
|
put("title", item.title)
|
|
78
83
|
item.description?.let { put("description", it) }
|
|
79
84
|
put("duration", item.duration)
|
|
@@ -138,29 +143,40 @@ class ShortKitBridge(
|
|
|
138
143
|
return try {
|
|
139
144
|
val obj = JSONObject(json)
|
|
140
145
|
|
|
146
|
+
val overlayRaw = obj.optString("overlay", null)
|
|
147
|
+
|
|
141
148
|
val feedHeight = parseFeedHeight(obj.optString("feedHeight", null))
|
|
142
149
|
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
143
|
-
val videoOverlay = parseVideoOverlay(
|
|
150
|
+
val videoOverlay = parseVideoOverlay(overlayRaw, context)
|
|
144
151
|
|
|
145
152
|
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
146
153
|
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
147
154
|
|
|
148
|
-
val
|
|
155
|
+
val carouselOverlayRaw = obj.optString("carouselOverlay", null)
|
|
156
|
+
val carouselOverlay = parseCarouselOverlay(carouselOverlayRaw, context)
|
|
157
|
+
val videoCarouselOverlayRaw = obj.optString("videoCarouselOverlay", null)
|
|
158
|
+
val videoCarouselOverlay = parseVideoCarouselOverlay(videoCarouselOverlayRaw, context)
|
|
149
159
|
val autoplay = obj.optBoolean("autoplay", true)
|
|
150
160
|
val filter = obj.optJSONObject("filter")?.let { parseFeedFilterToModel(it.toString()) }
|
|
151
161
|
|
|
162
|
+
val scrollAxisStr = obj.optString("scrollAxis", "vertical")
|
|
163
|
+
val scrollAxis = if (scrollAxisStr == "horizontal") ScrollAxis.Horizontal else ScrollAxis.Vertical
|
|
164
|
+
|
|
152
165
|
FeedConfig(
|
|
153
166
|
feedHeight = feedHeight,
|
|
154
167
|
videoOverlay = videoOverlay,
|
|
155
168
|
carouselOverlay = carouselOverlay,
|
|
169
|
+
videoCarouselOverlay = videoCarouselOverlay,
|
|
156
170
|
surveyOverlay = SurveyOverlayMode.None,
|
|
157
171
|
adOverlay = AdOverlayMode.None,
|
|
158
172
|
muteOnStart = muteOnStart,
|
|
159
173
|
autoplay = autoplay,
|
|
160
174
|
feedSource = feedSource,
|
|
161
175
|
filter = filter,
|
|
176
|
+
scrollAxis = scrollAxis,
|
|
162
177
|
)
|
|
163
|
-
} catch (
|
|
178
|
+
} catch (e: Exception) {
|
|
179
|
+
android.util.Log.e("SK:Bridge", "parseFeedConfig: EXCEPTION parsing config, returning default. json=${json.take(300)}", e)
|
|
164
180
|
FeedConfig()
|
|
165
181
|
}
|
|
166
182
|
}
|
|
@@ -191,7 +207,9 @@ class ShortKitBridge(
|
|
|
191
207
|
* - `"{\"type\":\"custom\"}"` -> Custom with ReactOverlayHost factory
|
|
192
208
|
*/
|
|
193
209
|
private fun parseVideoOverlay(json: String?, context: android.content.Context?): VideoOverlayMode {
|
|
194
|
-
if (json.isNullOrEmpty())
|
|
210
|
+
if (json.isNullOrEmpty()) {
|
|
211
|
+
return VideoOverlayMode.None
|
|
212
|
+
}
|
|
195
213
|
return try {
|
|
196
214
|
val parsed = json.trim()
|
|
197
215
|
|
|
@@ -205,14 +223,18 @@ class ShortKitBridge(
|
|
|
205
223
|
JSONObject(parsed)
|
|
206
224
|
}
|
|
207
225
|
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
val type = inner.optString("type")
|
|
227
|
+
val name = inner.optString("name", "Default")
|
|
228
|
+
|
|
229
|
+
if (type == "custom" && context != null) {
|
|
210
230
|
val ctx = context.applicationContext
|
|
211
231
|
VideoOverlayMode.Custom { ReactOverlayHost(ctx).apply { overlayName = name } }
|
|
212
232
|
} else {
|
|
233
|
+
android.util.Log.w("SK:Bridge", "parseVideoOverlay: → None (type='$type' context=${if (context != null) "OK" else "NULL"})")
|
|
213
234
|
VideoOverlayMode.None
|
|
214
235
|
}
|
|
215
|
-
} catch (
|
|
236
|
+
} catch (e: Exception) {
|
|
237
|
+
android.util.Log.e("SK:Bridge", "parseVideoOverlay: EXCEPTION → None. json=${json.take(200)}", e)
|
|
216
238
|
VideoOverlayMode.None
|
|
217
239
|
}
|
|
218
240
|
}
|
|
@@ -256,6 +278,43 @@ class ShortKitBridge(
|
|
|
256
278
|
}
|
|
257
279
|
}
|
|
258
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Parse a double-stringified video carousel overlay JSON.
|
|
283
|
+
* - `"\"none\""` -> None
|
|
284
|
+
* - `"{\"type\":\"custom\"}"` -> Custom with ReactVideoCarouselOverlayHost factory
|
|
285
|
+
*/
|
|
286
|
+
private fun parseVideoCarouselOverlay(json: String?, context: android.content.Context?): VideoCarouselOverlayMode {
|
|
287
|
+
if (json.isNullOrEmpty()) return VideoCarouselOverlayMode.None
|
|
288
|
+
return try {
|
|
289
|
+
val parsed = json.trim()
|
|
290
|
+
|
|
291
|
+
if (parsed == "\"none\"" || parsed == "none") {
|
|
292
|
+
return VideoCarouselOverlayMode.None
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
|
|
296
|
+
JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
|
|
297
|
+
} else {
|
|
298
|
+
JSONObject(parsed)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (inner.optString("type") == "custom" && context != null) {
|
|
302
|
+
val name = inner.optString("name", "Default")
|
|
303
|
+
val ctx = context.applicationContext
|
|
304
|
+
VideoCarouselOverlayMode.Custom {
|
|
305
|
+
ReactVideoCarouselOverlayHost(ctx).apply {
|
|
306
|
+
videoCarouselOverlayName = name
|
|
307
|
+
prepareSurface()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
VideoCarouselOverlayMode.None
|
|
312
|
+
}
|
|
313
|
+
} catch (_: Exception) {
|
|
314
|
+
VideoCarouselOverlayMode.None
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
259
318
|
private fun parseFeedFilter(json: String?): String? {
|
|
260
319
|
// On Android, the SDK accepts the raw JSON string for filtering.
|
|
261
320
|
// Return as-is if non-null/non-empty.
|
|
@@ -299,6 +358,26 @@ class ShortKitBridge(
|
|
|
299
358
|
val carouselItem = parseImageCarouselItem(itemObj) ?: continue
|
|
300
359
|
result.add(FeedInput.ImageCarousel(carouselItem))
|
|
301
360
|
}
|
|
361
|
+
"videoCarousel" -> {
|
|
362
|
+
val itemObj = obj.optJSONObject("item") ?: continue
|
|
363
|
+
val videosArr = itemObj.optJSONArray("videos") ?: continue
|
|
364
|
+
val videos = mutableListOf<ContentItem>()
|
|
365
|
+
for (i in 0 until videosArr.length()) {
|
|
366
|
+
val videoObj = videosArr.getJSONObject(i)
|
|
367
|
+
parseContentItem(videoObj)?.let { videos.add(it) }
|
|
368
|
+
}
|
|
369
|
+
if (videos.isEmpty()) continue
|
|
370
|
+
val carouselItem = VideoCarouselItem(
|
|
371
|
+
id = itemObj.getString("id"),
|
|
372
|
+
videos = videos,
|
|
373
|
+
title = itemObj.optString("title", null),
|
|
374
|
+
description = itemObj.optString("description", null),
|
|
375
|
+
author = itemObj.optString("author", null),
|
|
376
|
+
section = itemObj.optString("section", null),
|
|
377
|
+
articleUrl = itemObj.optString("articleUrl", null),
|
|
378
|
+
)
|
|
379
|
+
result.add(FeedInput.VideoCarousel(carouselItem))
|
|
380
|
+
}
|
|
302
381
|
}
|
|
303
382
|
}
|
|
304
383
|
result.ifEmpty { null }
|
|
@@ -332,6 +411,27 @@ class ShortKitBridge(
|
|
|
332
411
|
)
|
|
333
412
|
}
|
|
334
413
|
|
|
414
|
+
private fun parseContentItem(obj: JSONObject): ContentItem? {
|
|
415
|
+
val id = obj.optString("id", null) ?: return null
|
|
416
|
+
val title = obj.optString("title", null) ?: return null
|
|
417
|
+
val duration = obj.optDouble("duration", -1.0).takeIf { it >= 0 } ?: return null
|
|
418
|
+
val streamingUrl = obj.optString("streamingUrl", null) ?: return null
|
|
419
|
+
val thumbnailUrl = obj.optString("thumbnailUrl", null) ?: return null
|
|
420
|
+
return ContentItem(
|
|
421
|
+
id = id,
|
|
422
|
+
playbackId = obj.optString("playbackId", null),
|
|
423
|
+
title = title,
|
|
424
|
+
description = obj.optString("description", null),
|
|
425
|
+
duration = duration,
|
|
426
|
+
streamingUrl = streamingUrl,
|
|
427
|
+
thumbnailUrl = thumbnailUrl,
|
|
428
|
+
captionTracks = emptyList(),
|
|
429
|
+
author = obj.optString("author", null),
|
|
430
|
+
articleUrl = obj.optString("articleUrl", null),
|
|
431
|
+
fallbackUrl = obj.optString("fallbackUrl", null),
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
335
435
|
private fun buildCaptionTracksJSONArray(tracks: List<CaptionTrack>): JSONArray {
|
|
336
436
|
val arr = JSONArray()
|
|
337
437
|
for (track in tracks) {
|
|
@@ -378,7 +478,7 @@ class ShortKitBridge(
|
|
|
378
478
|
private var scope: CoroutineScope? = null
|
|
379
479
|
|
|
380
480
|
/** Preload handles keyed by UUID, consumed by feed views via preloadId prop. */
|
|
381
|
-
var preloadHandles = mutableMapOf<String,
|
|
481
|
+
var preloadHandles = mutableMapOf<String, FeedPreload>()
|
|
382
482
|
private set
|
|
383
483
|
|
|
384
484
|
// ------------------------------------------------------------------
|
|
@@ -450,8 +550,11 @@ class ShortKitBridge(
|
|
|
450
550
|
pending = staticPendingFeedViews.toList()
|
|
451
551
|
staticPendingFeedViews.clear()
|
|
452
552
|
}
|
|
553
|
+
val mainHandler = Handler(Looper.getMainLooper())
|
|
453
554
|
for (view in pending) {
|
|
454
|
-
|
|
555
|
+
mainHandler.post {
|
|
556
|
+
if (view.isAttachedToWindow) view.embedFeedFragmentIfNeeded()
|
|
557
|
+
}
|
|
455
558
|
}
|
|
456
559
|
}
|
|
457
560
|
|
|
@@ -490,19 +593,29 @@ class ShortKitBridge(
|
|
|
490
593
|
}
|
|
491
594
|
emitEventOnMain("onRemainingContentCountChanged", params)
|
|
492
595
|
}
|
|
596
|
+
fragment.onFeedReady = {
|
|
597
|
+
val params = Arguments.createMap().apply {
|
|
598
|
+
putString("feedId", id)
|
|
599
|
+
}
|
|
600
|
+
emitEventOnMain("onFeedReady", params)
|
|
601
|
+
}
|
|
493
602
|
|
|
494
|
-
// Replay buffered operations
|
|
603
|
+
// Replay buffered operations after the fragment's view is created.
|
|
604
|
+
// commitNowAllowingStateLoss triggers onCreate but onViewCreated
|
|
605
|
+
// (which sets up customFeedController) runs on a later main-thread
|
|
606
|
+
// tick. A single post isn't enough — use postDelayed to ensure the
|
|
607
|
+
// fragment's view lifecycle has completed.
|
|
495
608
|
val ops: List<(ShortKitFeedFragment) -> Unit>?
|
|
496
609
|
synchronized(pendingOpsLock) {
|
|
497
610
|
ops = pendingOps.remove(id)?.toList()
|
|
498
611
|
}
|
|
499
612
|
if (ops != null) {
|
|
500
|
-
Handler(Looper.getMainLooper()).
|
|
501
|
-
val frag = feedRegistry[id]?.get() ?: return@
|
|
613
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
614
|
+
val frag = feedRegistry[id]?.get() ?: return@postDelayed
|
|
502
615
|
for (op in ops) {
|
|
503
616
|
op(frag)
|
|
504
617
|
}
|
|
505
|
-
}
|
|
618
|
+
}, 100)
|
|
506
619
|
}
|
|
507
620
|
}
|
|
508
621
|
|
|
@@ -544,7 +657,7 @@ class ShortKitBridge(
|
|
|
544
657
|
// Preload handle management
|
|
545
658
|
// ------------------------------------------------------------------
|
|
546
659
|
|
|
547
|
-
fun consumePreload(id: String):
|
|
660
|
+
fun consumePreload(id: String): FeedPreload? {
|
|
548
661
|
return preloadHandles.remove(id)
|
|
549
662
|
}
|
|
550
663
|
|
|
@@ -714,13 +827,37 @@ class ShortKitBridge(
|
|
|
714
827
|
// Preload feed
|
|
715
828
|
// ------------------------------------------------------------------
|
|
716
829
|
|
|
717
|
-
fun preloadFeed(configJSON: String, callback: (String) -> Unit) {
|
|
830
|
+
fun preloadFeed(configJSON: String, itemsJSON: String?, callback: (String) -> Unit) {
|
|
718
831
|
val sdk = shortKit
|
|
719
832
|
if (sdk == null) {
|
|
720
833
|
callback("")
|
|
721
834
|
return
|
|
722
835
|
}
|
|
723
|
-
val
|
|
836
|
+
val config = parseFeedConfig(configJSON)
|
|
837
|
+
if (config.feedSource == FeedSource.CUSTOM) {
|
|
838
|
+
if (itemsJSON == null) {
|
|
839
|
+
android.util.Log.w("ShortKit", "preloadFeed called with feedSource=CUSTOM but no items")
|
|
840
|
+
callback("")
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
val items = parseFeedInputs(itemsJSON)
|
|
844
|
+
if (items == null) {
|
|
845
|
+
callback("")
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
val preload = sdk.preloadFeed(items = items)
|
|
849
|
+
val uuid = java.util.UUID.randomUUID().toString()
|
|
850
|
+
preloadHandles[uuid] = preload
|
|
851
|
+
callback(uuid)
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
val filterJSON = config.filter?.let { org.json.JSONObject().apply {
|
|
855
|
+
it.tags?.let { tags -> put("tags", org.json.JSONArray(tags)) }
|
|
856
|
+
it.section?.let { s -> put("section", s) }
|
|
857
|
+
it.author?.let { a -> put("author", a) }
|
|
858
|
+
it.contentType?.let { ct -> put("contentType", ct) }
|
|
859
|
+
}.toString() }
|
|
860
|
+
val preload = sdk.preloadFeed(filter = filterJSON)
|
|
724
861
|
val uuid = java.util.UUID.randomUUID().toString()
|
|
725
862
|
preloadHandles[uuid] = preload
|
|
726
863
|
callback(uuid)
|
|
@@ -952,16 +1089,7 @@ class ShortKitBridge(
|
|
|
952
1089
|
}
|
|
953
1090
|
}
|
|
954
1091
|
|
|
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
|
-
}
|
|
1092
|
+
// Remaining content count — handled per-feed via fragment.onRemainingContentCountChange callback
|
|
965
1093
|
|
|
966
1094
|
// Feed scroll phase
|
|
967
1095
|
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
|
}
|