@shortkitsdk/react-native 0.2.0 → 0.2.2
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/ShortKitCarouselOverlayBridge.kt +48 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +180 -2
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -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 +134 -2
- package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
- package/ios/ShortKitFeedView.swift +46 -7
- package/ios/ShortKitModule.mm +42 -0
- package/ios/ShortKitOverlayBridge.swift +23 -1
- 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/CarouselOverlayManager.tsx +71 -0
- package/src/ShortKitContext.ts +18 -0
- package/src/ShortKitFeed.tsx +13 -0
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +161 -2
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +15 -1
- package/src/serialization.ts +16 -2
- package/src/specs/NativeShortKitModule.ts +37 -0
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +82 -3
- package/src/useShortKit.ts +5 -1
- package/src/useShortKitCarousel.ts +29 -0
- package/src/useShortKitPlayer.ts +10 -2
|
@@ -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
|
|
@@ -307,6 +347,20 @@ import ShortKit
|
|
|
307
347
|
emitOnMain(name, body: body)
|
|
308
348
|
}
|
|
309
349
|
|
|
350
|
+
// MARK: - Carousel Overlay Lifecycle Events
|
|
351
|
+
|
|
352
|
+
/// Emit carousel overlay lifecycle events with an ImageCarouselItem.
|
|
353
|
+
public func emitCarouselOverlayEvent(_ name: String, item: ImageCarouselItem) {
|
|
354
|
+
guard let data = try? JSONEncoder().encode(item),
|
|
355
|
+
let json = String(data: data, encoding: .utf8) else { return }
|
|
356
|
+
emitOnMain(name, body: ["item": json])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Emit a raw carousel overlay event with an arbitrary body.
|
|
360
|
+
public func emitCarouselOverlayEvent(_ name: String, body: [String: Any]) {
|
|
361
|
+
emitOnMain(name, body: body)
|
|
362
|
+
}
|
|
363
|
+
|
|
310
364
|
// MARK: - Content Item Serialization
|
|
311
365
|
|
|
312
366
|
/// Serialize a ContentItem to a JSON string for bridge transport.
|
|
@@ -329,6 +383,10 @@ import ShortKit
|
|
|
329
383
|
"thumbnailUrl": item.thumbnailUrl,
|
|
330
384
|
]
|
|
331
385
|
|
|
386
|
+
if let playbackId = item.playbackId {
|
|
387
|
+
dict["playbackId"] = playbackId
|
|
388
|
+
}
|
|
389
|
+
|
|
332
390
|
if let description = item.description {
|
|
333
391
|
dict["description"] = description
|
|
334
392
|
}
|
|
@@ -410,13 +468,19 @@ import ShortKit
|
|
|
410
468
|
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
411
469
|
let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
|
|
412
470
|
|
|
471
|
+
let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
|
|
472
|
+
let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
|
|
473
|
+
|
|
474
|
+
let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
|
|
475
|
+
|
|
413
476
|
return FeedConfig(
|
|
414
477
|
feedHeight: feedHeight,
|
|
415
478
|
videoOverlay: videoOverlay,
|
|
416
|
-
carouselOverlay:
|
|
479
|
+
carouselOverlay: carouselOverlay,
|
|
417
480
|
surveyOverlay: .none,
|
|
418
481
|
adOverlay: .none,
|
|
419
|
-
muteOnStart: muteOnStart
|
|
482
|
+
muteOnStart: muteOnStart,
|
|
483
|
+
feedSource: feedSource
|
|
420
484
|
)
|
|
421
485
|
}
|
|
422
486
|
|
|
@@ -451,6 +515,35 @@ import ShortKit
|
|
|
451
515
|
return .none
|
|
452
516
|
}
|
|
453
517
|
|
|
518
|
+
/// Parse a double-stringified carousel overlay JSON into a `CarouselOverlayMode`.
|
|
519
|
+
///
|
|
520
|
+
/// Examples:
|
|
521
|
+
/// - `"\"none\""` → `.none`
|
|
522
|
+
/// - `"{\"type\":\"custom\"}"` → `.custom { ShortKitCarouselOverlayBridge() }`
|
|
523
|
+
private static func parseCarouselOverlay(_ json: String?) -> CarouselOverlayMode {
|
|
524
|
+
guard let json,
|
|
525
|
+
let data = json.data(using: .utf8),
|
|
526
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) else {
|
|
527
|
+
return .none
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if let str = parsed as? String, str == "none" {
|
|
531
|
+
return .none
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if let obj = parsed as? [String: Any],
|
|
535
|
+
let type = obj["type"] as? String,
|
|
536
|
+
type == "custom" {
|
|
537
|
+
return .custom { @Sendable in
|
|
538
|
+
let overlay = ShortKitCarouselOverlayBridge()
|
|
539
|
+
overlay.bridge = ShortKitBridge.shared
|
|
540
|
+
return overlay
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return .none
|
|
545
|
+
}
|
|
546
|
+
|
|
454
547
|
/// Parse a double-stringified feedHeight JSON.
|
|
455
548
|
/// e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
|
|
456
549
|
private static func parseFeedHeight(_ json: String?) -> FeedHeight {
|
|
@@ -472,6 +565,34 @@ import ShortKit
|
|
|
472
565
|
}
|
|
473
566
|
}
|
|
474
567
|
|
|
568
|
+
/// Parse a JSON string of CustomFeedItem[] from the JS bridge.
|
|
569
|
+
private static func parseCustomFeedItems(_ json: String) -> [CustomFeedItem]? {
|
|
570
|
+
guard let data = json.data(using: .utf8),
|
|
571
|
+
let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
572
|
+
return nil
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
var result: [CustomFeedItem] = []
|
|
576
|
+
for obj in arr {
|
|
577
|
+
guard let type = obj["type"] as? String else { continue }
|
|
578
|
+
switch type {
|
|
579
|
+
case "video":
|
|
580
|
+
guard let playbackId = obj["playbackId"] as? String else { continue }
|
|
581
|
+
result.append(.video(playbackId: playbackId))
|
|
582
|
+
case "imageCarousel":
|
|
583
|
+
guard let itemData = obj["item"],
|
|
584
|
+
let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
|
|
585
|
+
let carouselItem = try? JSONDecoder().decode(ImageCarouselItem.self, from: itemJSON) else {
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
result.append(.imageCarousel(carouselItem))
|
|
589
|
+
default:
|
|
590
|
+
continue
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return result.isEmpty ? nil : result
|
|
594
|
+
}
|
|
595
|
+
|
|
475
596
|
/// Parse optional custom dimensions JSON string into dictionary.
|
|
476
597
|
private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
|
|
477
598
|
guard let json,
|
|
@@ -482,3 +603,14 @@ import ShortKit
|
|
|
482
603
|
return dict
|
|
483
604
|
}
|
|
484
605
|
}
|
|
606
|
+
|
|
607
|
+
// MARK: - ShortKitDelegate
|
|
608
|
+
|
|
609
|
+
extension ShortKitBridge: ShortKitDelegate {
|
|
610
|
+
public func shortKit(_ shortKit: ShortKit, didTapContent contentId: String, at index: Int) {
|
|
611
|
+
emitOnMain("onContentTapped", body: [
|
|
612
|
+
"contentId": contentId,
|
|
613
|
+
"index": index
|
|
614
|
+
])
|
|
615
|
+
}
|
|
616
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// A transparent UIView that conforms to `CarouselOverlay` and bridges
|
|
5
|
+
/// carousel overlay lifecycle calls to JS events via `ShortKitBridge`.
|
|
6
|
+
///
|
|
7
|
+
/// The actual carousel overlay UI is rendered by React Native on the JS side
|
|
8
|
+
/// through the `CarouselOverlayManager` component. This view simply relays
|
|
9
|
+
/// the SDK lifecycle events so the JS carousel overlay knows when to
|
|
10
|
+
/// configure, activate, reset, etc.
|
|
11
|
+
@objc public class ShortKitCarouselOverlayBridge: UIView, @unchecked Sendable, CarouselOverlay {
|
|
12
|
+
|
|
13
|
+
// MARK: - Bridge Reference
|
|
14
|
+
|
|
15
|
+
/// Weak reference to the bridge, set by the factory closure in `parseFeedConfig`.
|
|
16
|
+
weak var bridge: ShortKitBridge?
|
|
17
|
+
|
|
18
|
+
// MARK: - State
|
|
19
|
+
|
|
20
|
+
/// Stores the last configured ImageCarouselItem so we can pass it with
|
|
21
|
+
/// lifecycle events that don't receive the item as a parameter.
|
|
22
|
+
private var currentItem: ImageCarouselItem?
|
|
23
|
+
|
|
24
|
+
// MARK: - Init
|
|
25
|
+
|
|
26
|
+
override init(frame: CGRect) {
|
|
27
|
+
super.init(frame: frame)
|
|
28
|
+
backgroundColor = .clear
|
|
29
|
+
isUserInteractionEnabled = true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
required init?(coder: NSCoder) {
|
|
33
|
+
fatalError("init(coder:) is not supported")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - CarouselOverlay
|
|
37
|
+
|
|
38
|
+
public func configure(with item: ImageCarouselItem) {
|
|
39
|
+
currentItem = item
|
|
40
|
+
bridge?.emitCarouselOverlayEvent("onCarouselOverlayConfigure", item: item)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public func resetState() {
|
|
44
|
+
bridge?.emitCarouselOverlayEvent("onCarouselOverlayReset", body: [:])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func fadeOutForTransition() {
|
|
48
|
+
bridge?.emitCarouselOverlayEvent("onCarouselOverlayFadeOut", body: [:])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public func restoreFromTransition() {
|
|
52
|
+
bridge?.emitCarouselOverlayEvent("onCarouselOverlayRestore", body: [:])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -27,10 +27,14 @@ import ShortKit
|
|
|
27
27
|
// MARK: - Scroll Tracking
|
|
28
28
|
|
|
29
29
|
private var scrollObservation: NSKeyValueObservation?
|
|
30
|
-
///
|
|
30
|
+
/// Video overlay for the currently active cell (nativeID="overlay-current").
|
|
31
31
|
private weak var currentOverlayView: UIView?
|
|
32
|
-
///
|
|
32
|
+
/// Video overlay for the upcoming cell (nativeID="overlay-next").
|
|
33
33
|
private weak var nextOverlayView: UIView?
|
|
34
|
+
/// Carousel overlay for the currently active cell (nativeID="carousel-overlay-current").
|
|
35
|
+
private weak var currentCarouselOverlayView: UIView?
|
|
36
|
+
/// Carousel overlay for the upcoming cell (nativeID="carousel-overlay-next").
|
|
37
|
+
private weak var nextCarouselOverlayView: UIView?
|
|
34
38
|
/// The page index used for overlay transform calculations.
|
|
35
39
|
private var currentPage: Int = 0
|
|
36
40
|
/// Deferred page update to avoid flashing stale metadata.
|
|
@@ -94,8 +98,12 @@ import ShortKit
|
|
|
94
98
|
scrollObservation = nil
|
|
95
99
|
currentOverlayView?.transform = .identity
|
|
96
100
|
nextOverlayView?.transform = .identity
|
|
101
|
+
currentCarouselOverlayView?.transform = .identity
|
|
102
|
+
nextCarouselOverlayView?.transform = .identity
|
|
97
103
|
currentOverlayView = nil
|
|
98
104
|
nextOverlayView = nil
|
|
105
|
+
currentCarouselOverlayView = nil
|
|
106
|
+
nextCarouselOverlayView = nil
|
|
99
107
|
|
|
100
108
|
guard let feedVC = feedViewController else { return }
|
|
101
109
|
|
|
@@ -150,6 +158,8 @@ import ShortKit
|
|
|
150
158
|
let h = self.bounds.height
|
|
151
159
|
self.currentOverlayView?.transform = .identity
|
|
152
160
|
self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
|
|
161
|
+
self.currentCarouselOverlayView?.transform = .identity
|
|
162
|
+
self.nextCarouselOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
|
|
153
163
|
}
|
|
154
164
|
pageUpdateWorkItem = workItem
|
|
155
165
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
|
|
@@ -168,17 +178,27 @@ import ShortKit
|
|
|
168
178
|
if currentOverlayView == nil || nextOverlayView == nil {
|
|
169
179
|
findOverlayViews()
|
|
170
180
|
}
|
|
181
|
+
if currentCarouselOverlayView == nil || nextCarouselOverlayView == nil {
|
|
182
|
+
findCarouselOverlayViews()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let translateY = CGAffineTransform(translationX: 0, y: -delta)
|
|
171
186
|
|
|
172
|
-
// Current
|
|
173
|
-
currentOverlayView?.transform =
|
|
187
|
+
// Current overlays follow the active cell
|
|
188
|
+
currentOverlayView?.transform = translateY
|
|
189
|
+
currentCarouselOverlayView?.transform = translateY
|
|
174
190
|
|
|
175
|
-
// Next
|
|
191
|
+
// Next overlays: positioned one page ahead in the scroll direction
|
|
176
192
|
if delta >= 0 {
|
|
177
193
|
// Forward scroll (or idle): next cell is below
|
|
178
|
-
|
|
194
|
+
let nextTransform = CGAffineTransform(translationX: 0, y: cellHeight - delta)
|
|
195
|
+
nextOverlayView?.transform = nextTransform
|
|
196
|
+
nextCarouselOverlayView?.transform = nextTransform
|
|
179
197
|
} else {
|
|
180
198
|
// Backward scroll: next cell is above
|
|
181
|
-
|
|
199
|
+
let nextTransform = CGAffineTransform(translationX: 0, y: -cellHeight - delta)
|
|
200
|
+
nextOverlayView?.transform = nextTransform
|
|
201
|
+
nextCarouselOverlayView?.transform = nextTransform
|
|
182
202
|
}
|
|
183
203
|
}
|
|
184
204
|
|
|
@@ -208,6 +228,25 @@ import ShortKit
|
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
/// Find the sibling RN carousel overlay views by nativeID.
|
|
232
|
+
private func findCarouselOverlayViews() {
|
|
233
|
+
var ancestor: UIView? = superview
|
|
234
|
+
while let container = ancestor {
|
|
235
|
+
for child in container.subviews {
|
|
236
|
+
guard !self.isDescendant(of: child) else { continue }
|
|
237
|
+
|
|
238
|
+
if currentCarouselOverlayView == nil {
|
|
239
|
+
currentCarouselOverlayView = findView(withNativeID: "carousel-overlay-current", in: child)
|
|
240
|
+
}
|
|
241
|
+
if nextCarouselOverlayView == nil {
|
|
242
|
+
nextCarouselOverlayView = findView(withNativeID: "carousel-overlay-next", in: child)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if currentCarouselOverlayView != nil && nextCarouselOverlayView != nil { return }
|
|
246
|
+
ancestor = container.superview
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
211
250
|
/// Recursively find a view by its React Native `nativeID` prop.
|
|
212
251
|
/// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
|
|
213
252
|
/// not `accessibilityIdentifier`.
|
package/ios/ShortKitModule.mm
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
@implementation ShortKitModule {
|
|
12
12
|
ShortKitBridge *_shortKitBridge;
|
|
13
13
|
BOOL _hasListeners;
|
|
14
|
+
NSString *_pendingFeedItems;
|
|
15
|
+
NSString *_pendingAppendItems;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
RCT_EXPORT_MODULE(ShortKitModule)
|
|
@@ -54,6 +56,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
54
56
|
@"onFeedTransition",
|
|
55
57
|
@"onFormatChange",
|
|
56
58
|
@"onPrefetchedAheadCountChanged",
|
|
59
|
+
@"onRemainingContentCountChanged",
|
|
57
60
|
@"onError",
|
|
58
61
|
@"onShareTapped",
|
|
59
62
|
@"onSurveyResponse",
|
|
@@ -64,6 +67,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
64
67
|
@"onOverlayRestore",
|
|
65
68
|
@"onOverlayTap",
|
|
66
69
|
@"onOverlayDoubleTap",
|
|
70
|
+
@"onContentTapped",
|
|
67
71
|
];
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -102,6 +106,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
102
106
|
|
|
103
107
|
RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
104
108
|
config:(NSString *)config
|
|
109
|
+
embedId:(NSString *)embedId
|
|
105
110
|
clientAppName:(NSString *)clientAppName
|
|
106
111
|
clientAppVersion:(NSString *)clientAppVersion
|
|
107
112
|
customDimensions:(NSString *)customDimensions) {
|
|
@@ -110,10 +115,21 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
|
110
115
|
|
|
111
116
|
_shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
|
|
112
117
|
config:config
|
|
118
|
+
embedId:embedId
|
|
113
119
|
clientAppName:clientAppName
|
|
114
120
|
clientAppVersion:clientAppVersion
|
|
115
121
|
customDimensions:customDimensions
|
|
116
122
|
delegate:self];
|
|
123
|
+
|
|
124
|
+
// Replay any feed items that arrived before initialization
|
|
125
|
+
if (_pendingFeedItems) {
|
|
126
|
+
[_shortKitBridge setFeedItems:_pendingFeedItems];
|
|
127
|
+
_pendingFeedItems = nil;
|
|
128
|
+
}
|
|
129
|
+
if (_pendingAppendItems) {
|
|
130
|
+
[_shortKitBridge appendFeedItems:_pendingAppendItems];
|
|
131
|
+
_pendingAppendItems = nil;
|
|
132
|
+
}
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
RCT_EXPORT_METHOD(destroy) {
|
|
@@ -187,6 +203,32 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
|
|
|
187
203
|
[_shortKitBridge setMaxBitrate:bitrate];
|
|
188
204
|
}
|
|
189
205
|
|
|
206
|
+
// MARK: - Custom Feed
|
|
207
|
+
|
|
208
|
+
RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
|
|
209
|
+
if (_shortKitBridge) {
|
|
210
|
+
[_shortKitBridge setFeedItems:items];
|
|
211
|
+
} else {
|
|
212
|
+
_pendingFeedItems = items;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
|
|
217
|
+
if (_shortKitBridge) {
|
|
218
|
+
[_shortKitBridge appendFeedItems:items];
|
|
219
|
+
} else {
|
|
220
|
+
_pendingAppendItems = items;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
|
|
225
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
226
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
227
|
+
[_shortKitBridge fetchContent:limit completion:^(NSString *json) {
|
|
228
|
+
resolve(json);
|
|
229
|
+
}];
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
// MARK: - New Architecture (TurboModule)
|
|
191
233
|
|
|
192
234
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -20,6 +20,11 @@ import ShortKit
|
|
|
20
20
|
/// events that don't receive the item as a parameter.
|
|
21
21
|
private var currentItem: ContentItem?
|
|
22
22
|
|
|
23
|
+
/// Deferred configure emission — cancelled if `activatePlayback()` fires
|
|
24
|
+
/// on the same run-loop iteration (meaning this was a handleSwipe
|
|
25
|
+
/// re-configure of the active cell, not a prefetch for the next cell).
|
|
26
|
+
private var pendingConfigureWorkItem: DispatchWorkItem?
|
|
27
|
+
|
|
23
28
|
// MARK: - Init
|
|
24
29
|
|
|
25
30
|
override init(frame: CGRect) {
|
|
@@ -66,7 +71,19 @@ import ShortKit
|
|
|
66
71
|
|
|
67
72
|
public func configure(with item: ContentItem) {
|
|
68
73
|
currentItem = item
|
|
69
|
-
|
|
74
|
+
|
|
75
|
+
// Defer the configure event by one run-loop tick. If activatePlayback()
|
|
76
|
+
// fires before then (handleSwipe sequence: configure → reset → activate),
|
|
77
|
+
// the event is cancelled — preventing nextItem from being overwritten
|
|
78
|
+
// with the current cell's data.
|
|
79
|
+
pendingConfigureWorkItem?.cancel()
|
|
80
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
81
|
+
guard let self, let item = self.currentItem else { return }
|
|
82
|
+
self.bridge?.emitOverlayEvent("onOverlayConfigure", item: item)
|
|
83
|
+
self.pendingConfigureWorkItem = nil
|
|
84
|
+
}
|
|
85
|
+
pendingConfigureWorkItem = workItem
|
|
86
|
+
DispatchQueue.main.async(execute: workItem)
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
public func resetPlaybackProgress() {
|
|
@@ -75,6 +92,11 @@ import ShortKit
|
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
public func activatePlayback() {
|
|
95
|
+
// Cancel the pending configure — it was for the current cell (part of
|
|
96
|
+
// handleSwipe), not a prefetch for the next cell.
|
|
97
|
+
pendingConfigureWorkItem?.cancel()
|
|
98
|
+
pendingConfigureWorkItem = nil
|
|
99
|
+
|
|
78
100
|
guard let item = currentItem else { return }
|
|
79
101
|
bridge?.emitOverlayEvent("onOverlayActivate", item: item)
|
|
80
102
|
}
|