@shortkitsdk/react-native 0.2.0 → 0.2.1
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/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +117 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
- package/ios/ShortKitBridge.swift +88 -1
- package/ios/ShortKitModule.mm +22 -0
- package/ios/ShortKitPlayerNativeView.swift +186 -0
- package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
- package/ios/ShortKitWidgetNativeView.swift +168 -0
- package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
- package/package.json +1 -1
- package/src/ShortKitContext.ts +5 -0
- package/src/ShortKitFeed.tsx +10 -0
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +43 -0
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +12 -0
- package/src/serialization.ts +10 -0
- package/src/specs/NativeShortKitModule.ts +18 -0
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +78 -0
- package/src/useShortKit.ts +5 -1
|
@@ -5,10 +5,14 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
5
5
|
import com.facebook.react.bridge.ReactMethod
|
|
6
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
7
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
8
|
+
import com.shortkit.CarouselImage
|
|
8
9
|
import com.shortkit.ContentItem
|
|
9
10
|
import com.shortkit.ContentSignal
|
|
11
|
+
import com.shortkit.CustomFeedItem
|
|
12
|
+
import com.shortkit.ImageCarouselItem
|
|
10
13
|
import com.shortkit.FeedConfig
|
|
11
14
|
import com.shortkit.FeedHeight
|
|
15
|
+
import com.shortkit.FeedSource
|
|
12
16
|
import com.shortkit.FeedTransitionPhase
|
|
13
17
|
import com.shortkit.JsonValue
|
|
14
18
|
import com.shortkit.ShortKit
|
|
@@ -89,6 +93,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
89
93
|
override fun initialize(
|
|
90
94
|
apiKey: String,
|
|
91
95
|
config: String,
|
|
96
|
+
embedId: String?,
|
|
92
97
|
clientAppName: String?,
|
|
93
98
|
clientAppVersion: String?,
|
|
94
99
|
customDimensions: String?
|
|
@@ -105,6 +110,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
105
110
|
context = context,
|
|
106
111
|
apiKey = apiKey,
|
|
107
112
|
config = feedConfig,
|
|
113
|
+
embedId = embedId,
|
|
108
114
|
userId = null,
|
|
109
115
|
adProvider = null,
|
|
110
116
|
clientAppName = clientAppName,
|
|
@@ -115,6 +121,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
115
121
|
shared = this
|
|
116
122
|
|
|
117
123
|
subscribeToFlows(sdk.player)
|
|
124
|
+
|
|
125
|
+
sdk.delegate = object : com.shortkit.ShortKitDelegate {
|
|
126
|
+
override fun onContentTapped(contentId: String, index: Int) {
|
|
127
|
+
val params = Arguments.createMap().apply {
|
|
128
|
+
putString("contentId", contentId)
|
|
129
|
+
putInt("index", index)
|
|
130
|
+
}
|
|
131
|
+
sendEvent("onContentTapped", params)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
@ReactMethod
|
|
@@ -211,6 +227,43 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
211
227
|
shortKit?.player?.setMaxBitrate(bitrate.toInt())
|
|
212
228
|
}
|
|
213
229
|
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
// Custom feed
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
@ReactMethod
|
|
235
|
+
override fun setFeedItems(items: String) {
|
|
236
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
237
|
+
shortKit?.setFeedItems(parsed)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@ReactMethod
|
|
241
|
+
override fun appendFeedItems(items: String) {
|
|
242
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
243
|
+
shortKit?.appendFeedItems(parsed)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@ReactMethod
|
|
247
|
+
override fun fetchContent(limit: Double, promise: com.facebook.react.bridge.Promise) {
|
|
248
|
+
val sdk = shortKit
|
|
249
|
+
if (sdk == null) {
|
|
250
|
+
promise.resolve("[]")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
scope?.launch {
|
|
254
|
+
try {
|
|
255
|
+
val items = sdk.fetchContent(limit.toInt())
|
|
256
|
+
val arr = JSONArray()
|
|
257
|
+
for (item in items) {
|
|
258
|
+
arr.put(JSONObject(serializeContentItemToJSON(item)))
|
|
259
|
+
}
|
|
260
|
+
promise.resolve(arr.toString())
|
|
261
|
+
} catch (e: Exception) {
|
|
262
|
+
promise.resolve("[]")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
214
267
|
// -----------------------------------------------------------------------
|
|
215
268
|
// Overlay lifecycle events (called by Fabric view)
|
|
216
269
|
// -----------------------------------------------------------------------
|
|
@@ -385,6 +438,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
385
438
|
sendEvent("onPrefetchedAheadCountChanged", params)
|
|
386
439
|
}
|
|
387
440
|
}
|
|
441
|
+
|
|
442
|
+
// Remaining content count
|
|
443
|
+
newScope.launch {
|
|
444
|
+
player.remainingContentCount.collect { count ->
|
|
445
|
+
val params = Arguments.createMap().apply {
|
|
446
|
+
putInt("count", count)
|
|
447
|
+
}
|
|
448
|
+
sendEvent("onRemainingContentCountChanged", params)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
388
451
|
}
|
|
389
452
|
|
|
390
453
|
// -----------------------------------------------------------------------
|
|
@@ -425,6 +488,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
425
488
|
private fun contentItemMap(item: ContentItem): WritableMap {
|
|
426
489
|
return Arguments.createMap().apply {
|
|
427
490
|
putString("id", item.id)
|
|
491
|
+
item.playbackId?.let { putString("playbackId", it) }
|
|
428
492
|
putString("title", item.title)
|
|
429
493
|
item.description?.let { putString("description", it) }
|
|
430
494
|
putDouble("duration", item.duration)
|
|
@@ -575,13 +639,17 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
575
639
|
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
576
640
|
val videoOverlay = parseVideoOverlay(obj.optString("overlay", null))
|
|
577
641
|
|
|
642
|
+
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
643
|
+
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
644
|
+
|
|
578
645
|
FeedConfig(
|
|
579
646
|
feedHeight = feedHeight,
|
|
580
647
|
videoOverlay = videoOverlay,
|
|
581
648
|
carouselOverlay = CarouselOverlayMode.None,
|
|
582
649
|
surveyOverlay = SurveyOverlayMode.None,
|
|
583
650
|
adOverlay = AdOverlayMode.None,
|
|
584
|
-
muteOnStart = muteOnStart
|
|
651
|
+
muteOnStart = muteOnStart,
|
|
652
|
+
feedSource = feedSource
|
|
585
653
|
)
|
|
586
654
|
} catch (_: Exception) {
|
|
587
655
|
FeedConfig()
|
|
@@ -644,6 +712,54 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
644
712
|
}
|
|
645
713
|
}
|
|
646
714
|
|
|
715
|
+
private fun parseCustomFeedItems(json: String): List<CustomFeedItem>? {
|
|
716
|
+
return try {
|
|
717
|
+
val arr = JSONArray(json)
|
|
718
|
+
val result = mutableListOf<CustomFeedItem>()
|
|
719
|
+
for (i in 0 until arr.length()) {
|
|
720
|
+
val obj = arr.getJSONObject(i)
|
|
721
|
+
when (obj.optString("type")) {
|
|
722
|
+
"video" -> {
|
|
723
|
+
val playbackId = obj.optString("playbackId", null) ?: continue
|
|
724
|
+
result.add(CustomFeedItem.Video(playbackId))
|
|
725
|
+
}
|
|
726
|
+
"imageCarousel" -> {
|
|
727
|
+
val itemObj = obj.optJSONObject("item") ?: continue
|
|
728
|
+
val carouselItem = parseImageCarouselItem(itemObj) ?: continue
|
|
729
|
+
result.add(CustomFeedItem.ImageCarousel(carouselItem))
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
result.ifEmpty { null }
|
|
734
|
+
} catch (_: Exception) {
|
|
735
|
+
null
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
|
|
740
|
+
val id = obj.optString("id", null) ?: return null
|
|
741
|
+
val imagesArr = obj.optJSONArray("images") ?: return null
|
|
742
|
+
val images = mutableListOf<CarouselImage>()
|
|
743
|
+
for (i in 0 until imagesArr.length()) {
|
|
744
|
+
val imgObj = imagesArr.getJSONObject(i)
|
|
745
|
+
images.add(CarouselImage(
|
|
746
|
+
url = imgObj.getString("url"),
|
|
747
|
+
alt = imgObj.optString("alt", null)
|
|
748
|
+
))
|
|
749
|
+
}
|
|
750
|
+
return ImageCarouselItem(
|
|
751
|
+
id = id,
|
|
752
|
+
images = images,
|
|
753
|
+
autoScrollInterval = if (obj.has("autoScrollInterval")) obj.getDouble("autoScrollInterval") else null,
|
|
754
|
+
caption = obj.optString("caption", null),
|
|
755
|
+
title = obj.optString("title", null),
|
|
756
|
+
description = obj.optString("description", null),
|
|
757
|
+
author = obj.optString("author", null),
|
|
758
|
+
section = obj.optString("section", null),
|
|
759
|
+
articleUrl = obj.optString("articleUrl", null)
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
647
763
|
/**
|
|
648
764
|
* Parse optional custom dimensions JSON string into map.
|
|
649
765
|
*/
|
|
@@ -35,6 +35,10 @@ class ShortKitPackage : TurboReactPackage() {
|
|
|
35
35
|
override fun createViewManagers(
|
|
36
36
|
reactContext: ReactApplicationContext
|
|
37
37
|
): List<ViewManager<*, *>> {
|
|
38
|
-
return listOf(
|
|
38
|
+
return listOf(
|
|
39
|
+
ShortKitFeedViewManager(),
|
|
40
|
+
ShortKitPlayerViewManager(),
|
|
41
|
+
ShortKitWidgetViewManager(),
|
|
42
|
+
)
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import com.shortkit.sdk.config.PlayerClickAction
|
|
6
|
+
import com.shortkit.sdk.config.PlayerConfig
|
|
7
|
+
import com.shortkit.sdk.config.VideoOverlayMode
|
|
8
|
+
import com.shortkit.sdk.model.ContentItem
|
|
9
|
+
import com.shortkit.sdk.player.ShortKitPlayerView
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fabric native view wrapping [ShortKitPlayerView] for use as a
|
|
14
|
+
* single-video player in React Native.
|
|
15
|
+
*/
|
|
16
|
+
class ShortKitPlayerNativeView(context: Context) : FrameLayout(context) {
|
|
17
|
+
|
|
18
|
+
private var playerView: ShortKitPlayerView? = null
|
|
19
|
+
private var configJson: String? = null
|
|
20
|
+
private var contentItemJson: String? = null
|
|
21
|
+
|
|
22
|
+
var config: String?
|
|
23
|
+
get() = configJson
|
|
24
|
+
set(value) {
|
|
25
|
+
if (value == configJson) return
|
|
26
|
+
configJson = value
|
|
27
|
+
rebuildIfNeeded()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var contentItem: String?
|
|
31
|
+
get() = contentItemJson
|
|
32
|
+
set(value) {
|
|
33
|
+
if (value == contentItemJson) return
|
|
34
|
+
contentItemJson = value
|
|
35
|
+
applyContentItem()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var active: Boolean = true
|
|
39
|
+
set(value) {
|
|
40
|
+
if (field == value) return
|
|
41
|
+
field = value
|
|
42
|
+
applyActive()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onAttachedToWindow() {
|
|
46
|
+
super.onAttachedToWindow()
|
|
47
|
+
rebuildIfNeeded()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onDetachedFromWindow() {
|
|
51
|
+
playerView?.deactivate()
|
|
52
|
+
super.onDetachedFromWindow()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun rebuildIfNeeded() {
|
|
56
|
+
if (playerView != null) return
|
|
57
|
+
|
|
58
|
+
val sdk = ShortKitModule.shared?.sdk ?: return
|
|
59
|
+
val playerConfig = parsePlayerConfig(configJson)
|
|
60
|
+
|
|
61
|
+
val view = ShortKitPlayerView(context).apply {
|
|
62
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
63
|
+
}
|
|
64
|
+
view.initialize(sdk, playerConfig)
|
|
65
|
+
addView(view)
|
|
66
|
+
playerView = view
|
|
67
|
+
|
|
68
|
+
applyContentItem()
|
|
69
|
+
if (active) view.activate()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun applyContentItem() {
|
|
73
|
+
val json = contentItemJson ?: return
|
|
74
|
+
val view = playerView ?: return
|
|
75
|
+
val item = parseContentItem(json) ?: return
|
|
76
|
+
view.configure(item)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun applyActive() {
|
|
80
|
+
val view = playerView ?: return
|
|
81
|
+
if (active) view.activate() else view.deactivate()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private fun parsePlayerConfig(json: String?): PlayerConfig {
|
|
85
|
+
if (json.isNullOrEmpty()) return PlayerConfig()
|
|
86
|
+
return try {
|
|
87
|
+
val obj = JSONObject(json)
|
|
88
|
+
PlayerConfig(
|
|
89
|
+
cornerRadius = obj.optDouble("cornerRadius", 12.0).toFloat(),
|
|
90
|
+
clickAction = when (obj.optString("clickAction", "feed")) {
|
|
91
|
+
"feed" -> PlayerClickAction.FEED
|
|
92
|
+
"mute" -> PlayerClickAction.MUTE
|
|
93
|
+
"none" -> PlayerClickAction.NONE
|
|
94
|
+
else -> PlayerClickAction.FEED
|
|
95
|
+
},
|
|
96
|
+
autoplay = obj.optBoolean("autoplay", true),
|
|
97
|
+
loop = obj.optBoolean("loop", true),
|
|
98
|
+
muteOnStart = obj.optBoolean("muteOnStart", true),
|
|
99
|
+
videoOverlay = parseOverlay(obj),
|
|
100
|
+
)
|
|
101
|
+
} catch (_: Exception) {
|
|
102
|
+
PlayerConfig()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun parseOverlay(obj: JSONObject): VideoOverlayMode {
|
|
107
|
+
val overlay = obj.opt("overlay") ?: return VideoOverlayMode.None
|
|
108
|
+
if (overlay is JSONObject && overlay.optString("type") == "custom") {
|
|
109
|
+
return VideoOverlayMode.Custom {
|
|
110
|
+
ShortKitOverlayBridge(context)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return VideoOverlayMode.None
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun parseContentItem(json: String): ContentItem? {
|
|
117
|
+
return try {
|
|
118
|
+
val obj = JSONObject(json)
|
|
119
|
+
ContentItem(
|
|
120
|
+
id = obj.getString("id"),
|
|
121
|
+
title = obj.getString("title"),
|
|
122
|
+
description = obj.optString("description", null),
|
|
123
|
+
duration = obj.getDouble("duration"),
|
|
124
|
+
streamingUrl = obj.getString("streamingUrl"),
|
|
125
|
+
thumbnailUrl = obj.getString("thumbnailUrl"),
|
|
126
|
+
captionTracks = emptyList(),
|
|
127
|
+
customMetadata = null,
|
|
128
|
+
author = obj.optString("author", null),
|
|
129
|
+
articleUrl = obj.optString("articleUrl", null),
|
|
130
|
+
commentCount = if (obj.has("commentCount")) obj.getInt("commentCount") else null,
|
|
131
|
+
)
|
|
132
|
+
} catch (_: Exception) {
|
|
133
|
+
null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = ShortKitPlayerViewManager.REACT_CLASS)
|
|
9
|
+
class ShortKitPlayerViewManager : SimpleViewManager<ShortKitPlayerNativeView>() {
|
|
10
|
+
|
|
11
|
+
override fun getName(): String = REACT_CLASS
|
|
12
|
+
|
|
13
|
+
override fun createViewInstance(context: ThemedReactContext): ShortKitPlayerNativeView {
|
|
14
|
+
return ShortKitPlayerNativeView(context)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@ReactProp(name = "config")
|
|
18
|
+
fun setConfig(view: ShortKitPlayerNativeView, config: String?) {
|
|
19
|
+
view.config = config
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@ReactProp(name = "contentItem")
|
|
23
|
+
fun setContentItem(view: ShortKitPlayerNativeView, contentItem: String?) {
|
|
24
|
+
view.contentItem = contentItem
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@ReactProp(name = "active", defaultBoolean = true)
|
|
28
|
+
fun setActive(view: ShortKitPlayerNativeView, active: Boolean) {
|
|
29
|
+
view.active = active
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
const val REACT_CLASS = "ShortKitPlayerView"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import com.shortkit.sdk.config.PlayerClickAction
|
|
6
|
+
import com.shortkit.sdk.config.VideoOverlayMode
|
|
7
|
+
import com.shortkit.sdk.config.WidgetConfig
|
|
8
|
+
import com.shortkit.sdk.model.ContentItem
|
|
9
|
+
import com.shortkit.sdk.widget.ShortKitWidgetView
|
|
10
|
+
import org.json.JSONArray
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fabric native view wrapping [ShortKitWidgetView] for use as a
|
|
15
|
+
* horizontal video carousel in React Native.
|
|
16
|
+
*/
|
|
17
|
+
class ShortKitWidgetNativeView(context: Context) : FrameLayout(context) {
|
|
18
|
+
|
|
19
|
+
private var widgetView: ShortKitWidgetView? = null
|
|
20
|
+
private var configJson: String? = null
|
|
21
|
+
private var itemsJson: String? = null
|
|
22
|
+
|
|
23
|
+
var config: String?
|
|
24
|
+
get() = configJson
|
|
25
|
+
set(value) {
|
|
26
|
+
if (value == configJson) return
|
|
27
|
+
configJson = value
|
|
28
|
+
rebuildIfNeeded()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var items: String?
|
|
32
|
+
get() = itemsJson
|
|
33
|
+
set(value) {
|
|
34
|
+
if (value == itemsJson) return
|
|
35
|
+
itemsJson = value
|
|
36
|
+
applyItems()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun onAttachedToWindow() {
|
|
40
|
+
super.onAttachedToWindow()
|
|
41
|
+
rebuildIfNeeded()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun onDetachedFromWindow() {
|
|
45
|
+
super.onDetachedFromWindow()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun rebuildIfNeeded() {
|
|
49
|
+
if (widgetView != null) return
|
|
50
|
+
|
|
51
|
+
val sdk = ShortKitModule.shared?.sdk ?: return
|
|
52
|
+
val widgetConfig = parseWidgetConfig(configJson)
|
|
53
|
+
|
|
54
|
+
val view = ShortKitWidgetView(context).apply {
|
|
55
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
56
|
+
}
|
|
57
|
+
view.initialize(sdk, widgetConfig)
|
|
58
|
+
addView(view)
|
|
59
|
+
widgetView = view
|
|
60
|
+
|
|
61
|
+
applyItems()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun applyItems() {
|
|
65
|
+
val json = itemsJson ?: return
|
|
66
|
+
val view = widgetView ?: return
|
|
67
|
+
val contentItems = parseContentItems(json) ?: return
|
|
68
|
+
view.configure(contentItems)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun parseWidgetConfig(json: String?): WidgetConfig {
|
|
72
|
+
if (json.isNullOrEmpty()) return WidgetConfig()
|
|
73
|
+
return try {
|
|
74
|
+
val obj = JSONObject(json)
|
|
75
|
+
WidgetConfig(
|
|
76
|
+
cardCount = obj.optInt("cardCount", 3),
|
|
77
|
+
cardSpacing = obj.optDouble("cardSpacing", 8.0).toFloat(),
|
|
78
|
+
cornerRadius = obj.optDouble("cornerRadius", 12.0).toFloat(),
|
|
79
|
+
autoplay = obj.optBoolean("autoplay", true),
|
|
80
|
+
muteOnStart = obj.optBoolean("muteOnStart", true),
|
|
81
|
+
loop = obj.optBoolean("loop", true),
|
|
82
|
+
rotationInterval = obj.optLong("rotationInterval", 10_000L),
|
|
83
|
+
clickAction = when (obj.optString("clickAction", "feed")) {
|
|
84
|
+
"feed" -> PlayerClickAction.FEED
|
|
85
|
+
"mute" -> PlayerClickAction.MUTE
|
|
86
|
+
"none" -> PlayerClickAction.NONE
|
|
87
|
+
else -> PlayerClickAction.FEED
|
|
88
|
+
},
|
|
89
|
+
cardOverlay = parseOverlay(obj),
|
|
90
|
+
)
|
|
91
|
+
} catch (_: Exception) {
|
|
92
|
+
WidgetConfig()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun parseOverlay(obj: JSONObject): VideoOverlayMode {
|
|
97
|
+
val overlay = obj.opt("overlay") ?: return VideoOverlayMode.None
|
|
98
|
+
if (overlay is JSONObject && overlay.optString("type") == "custom") {
|
|
99
|
+
return VideoOverlayMode.Custom {
|
|
100
|
+
ShortKitOverlayBridge(context)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return VideoOverlayMode.None
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun parseContentItems(json: String): List<ContentItem>? {
|
|
107
|
+
return try {
|
|
108
|
+
val arr = JSONArray(json)
|
|
109
|
+
val items = mutableListOf<ContentItem>()
|
|
110
|
+
for (i in 0 until arr.length()) {
|
|
111
|
+
val obj = arr.getJSONObject(i)
|
|
112
|
+
items.add(
|
|
113
|
+
ContentItem(
|
|
114
|
+
id = obj.getString("id"),
|
|
115
|
+
title = obj.getString("title"),
|
|
116
|
+
description = obj.optString("description", null),
|
|
117
|
+
duration = obj.getDouble("duration"),
|
|
118
|
+
streamingUrl = obj.getString("streamingUrl"),
|
|
119
|
+
thumbnailUrl = obj.getString("thumbnailUrl"),
|
|
120
|
+
captionTracks = emptyList(),
|
|
121
|
+
customMetadata = null,
|
|
122
|
+
author = obj.optString("author", null),
|
|
123
|
+
articleUrl = obj.optString("articleUrl", null),
|
|
124
|
+
commentCount = if (obj.has("commentCount")) obj.getInt("commentCount") else null,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
items
|
|
129
|
+
} catch (_: Exception) {
|
|
130
|
+
null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = ShortKitWidgetViewManager.REACT_CLASS)
|
|
9
|
+
class ShortKitWidgetViewManager : SimpleViewManager<ShortKitWidgetNativeView>() {
|
|
10
|
+
|
|
11
|
+
override fun getName(): String = REACT_CLASS
|
|
12
|
+
|
|
13
|
+
override fun createViewInstance(context: ThemedReactContext): ShortKitWidgetNativeView {
|
|
14
|
+
return ShortKitWidgetNativeView(context)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@ReactProp(name = "config")
|
|
18
|
+
fun setConfig(view: ShortKitWidgetNativeView, config: String?) {
|
|
19
|
+
view.config = config
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@ReactProp(name = "items")
|
|
23
|
+
fun setItems(view: ShortKitWidgetNativeView, items: String?) {
|
|
24
|
+
view.items = items
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
const val REACT_CLASS = "ShortKitWidgetView"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -23,6 +23,7 @@ import ShortKit
|
|
|
23
23
|
@objc public init(
|
|
24
24
|
apiKey: String,
|
|
25
25
|
config configJSON: String,
|
|
26
|
+
embedId: String?,
|
|
26
27
|
clientAppName: String?,
|
|
27
28
|
clientAppVersion: String?,
|
|
28
29
|
customDimensions customDimensionsJSON: String?,
|
|
@@ -37,6 +38,7 @@ import ShortKit
|
|
|
37
38
|
let sdk = ShortKit(
|
|
38
39
|
apiKey: apiKey,
|
|
39
40
|
config: feedConfig,
|
|
41
|
+
embedId: embedId,
|
|
40
42
|
clientAppName: clientAppName,
|
|
41
43
|
clientAppVersion: clientAppVersion,
|
|
42
44
|
customDimensions: dimensions
|
|
@@ -46,6 +48,7 @@ import ShortKit
|
|
|
46
48
|
ShortKitBridge.shared = self
|
|
47
49
|
|
|
48
50
|
subscribeToPublishers(sdk.player)
|
|
51
|
+
sdk.delegate = self
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
// MARK: - Teardown
|
|
@@ -140,6 +143,35 @@ import ShortKit
|
|
|
140
143
|
shortKit?.player.setMaxBitrate(Int(bitrate))
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
// MARK: - Custom Feed
|
|
147
|
+
|
|
148
|
+
@objc public func setFeedItems(_ json: String) {
|
|
149
|
+
guard let items = Self.parseCustomFeedItems(json) else { return }
|
|
150
|
+
Task { @MainActor in
|
|
151
|
+
self.shortKit?.setFeedItems(items)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@objc public func appendFeedItems(_ json: String) {
|
|
156
|
+
guard let items = Self.parseCustomFeedItems(json) else { return }
|
|
157
|
+
Task { @MainActor in
|
|
158
|
+
self.shortKit?.appendFeedItems(items)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@objc public func fetchContent(_ limit: Int, completion: @escaping (String) -> Void) {
|
|
163
|
+
Task {
|
|
164
|
+
do {
|
|
165
|
+
let items = try await self.shortKit?.fetchContent(limit: limit) ?? []
|
|
166
|
+
let data = try JSONEncoder().encode(items)
|
|
167
|
+
let json = String(data: data, encoding: .utf8) ?? "[]"
|
|
168
|
+
completion(json)
|
|
169
|
+
} catch {
|
|
170
|
+
completion("[]")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
// MARK: - Combine Subscriptions
|
|
144
176
|
|
|
145
177
|
private func subscribeToPublishers(_ player: ShortKitPlayer) {
|
|
@@ -277,6 +309,14 @@ import ShortKit
|
|
|
277
309
|
self?.emit("onPrefetchedAheadCountChanged", body: ["count": count])
|
|
278
310
|
}
|
|
279
311
|
.store(in: &cancellables)
|
|
312
|
+
|
|
313
|
+
// Remaining content count
|
|
314
|
+
player.remainingContentCount
|
|
315
|
+
.receive(on: DispatchQueue.main)
|
|
316
|
+
.sink { [weak self] count in
|
|
317
|
+
self?.emit("onRemainingContentCountChanged", body: ["count": count])
|
|
318
|
+
}
|
|
319
|
+
.store(in: &cancellables)
|
|
280
320
|
}
|
|
281
321
|
|
|
282
322
|
// MARK: - Event Emission Helpers
|
|
@@ -329,6 +369,10 @@ import ShortKit
|
|
|
329
369
|
"thumbnailUrl": item.thumbnailUrl,
|
|
330
370
|
]
|
|
331
371
|
|
|
372
|
+
if let playbackId = item.playbackId {
|
|
373
|
+
dict["playbackId"] = playbackId
|
|
374
|
+
}
|
|
375
|
+
|
|
332
376
|
if let description = item.description {
|
|
333
377
|
dict["description"] = description
|
|
334
378
|
}
|
|
@@ -410,13 +454,17 @@ import ShortKit
|
|
|
410
454
|
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
411
455
|
let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
|
|
412
456
|
|
|
457
|
+
let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
|
|
458
|
+
let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
|
|
459
|
+
|
|
413
460
|
return FeedConfig(
|
|
414
461
|
feedHeight: feedHeight,
|
|
415
462
|
videoOverlay: videoOverlay,
|
|
416
463
|
carouselOverlay: .none,
|
|
417
464
|
surveyOverlay: .none,
|
|
418
465
|
adOverlay: .none,
|
|
419
|
-
muteOnStart: muteOnStart
|
|
466
|
+
muteOnStart: muteOnStart,
|
|
467
|
+
feedSource: feedSource
|
|
420
468
|
)
|
|
421
469
|
}
|
|
422
470
|
|
|
@@ -472,6 +520,34 @@ import ShortKit
|
|
|
472
520
|
}
|
|
473
521
|
}
|
|
474
522
|
|
|
523
|
+
/// Parse a JSON string of CustomFeedItem[] from the JS bridge.
|
|
524
|
+
private static func parseCustomFeedItems(_ json: String) -> [CustomFeedItem]? {
|
|
525
|
+
guard let data = json.data(using: .utf8),
|
|
526
|
+
let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
527
|
+
return nil
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
var result: [CustomFeedItem] = []
|
|
531
|
+
for obj in arr {
|
|
532
|
+
guard let type = obj["type"] as? String else { continue }
|
|
533
|
+
switch type {
|
|
534
|
+
case "video":
|
|
535
|
+
guard let playbackId = obj["playbackId"] as? String else { continue }
|
|
536
|
+
result.append(.video(playbackId: playbackId))
|
|
537
|
+
case "imageCarousel":
|
|
538
|
+
guard let itemData = obj["item"],
|
|
539
|
+
let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
|
|
540
|
+
let carouselItem = try? JSONDecoder().decode(ImageCarouselItem.self, from: itemJSON) else {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
result.append(.imageCarousel(carouselItem))
|
|
544
|
+
default:
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return result.isEmpty ? nil : result
|
|
549
|
+
}
|
|
550
|
+
|
|
475
551
|
/// Parse optional custom dimensions JSON string into dictionary.
|
|
476
552
|
private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
|
|
477
553
|
guard let json,
|
|
@@ -482,3 +558,14 @@ import ShortKit
|
|
|
482
558
|
return dict
|
|
483
559
|
}
|
|
484
560
|
}
|
|
561
|
+
|
|
562
|
+
// MARK: - ShortKitDelegate
|
|
563
|
+
|
|
564
|
+
extension ShortKitBridge: ShortKitDelegate {
|
|
565
|
+
public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
|
|
566
|
+
emitOnMain("onContentTapped", body: [
|
|
567
|
+
"contentId": contentId,
|
|
568
|
+
"index": index
|
|
569
|
+
])
|
|
570
|
+
}
|
|
571
|
+
}
|