@shortkitsdk/react-native 0.1.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/ShortKitFeedView.kt +74 -19
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +107 -95
- 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 +81 -47
- package/ios/ShortKitFeedView.swift +67 -20
- package/ios/ShortKitModule.mm +22 -5
- 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 +5 -50
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +50 -4
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +12 -0
- package/src/serialization.ts +10 -0
- package/src/specs/NativeShortKitModule.ts +18 -25
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +78 -5
- package/src/useShortKit.ts +5 -1
|
@@ -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
|
@@ -6,7 +6,7 @@ import ShortKit
|
|
|
6
6
|
///
|
|
7
7
|
/// Holds the `ShortKit` instance, subscribes to all Combine publishers on
|
|
8
8
|
/// `ShortKitPlayer`, and forwards events to JS via the delegate protocol.
|
|
9
|
-
@objc public class ShortKitBridge: NSObject
|
|
9
|
+
@objc public class ShortKitBridge: NSObject {
|
|
10
10
|
|
|
11
11
|
// MARK: - Shared instance (accessed by Fabric view manager in Task 13)
|
|
12
12
|
|
|
@@ -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,16 +38,17 @@ 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
|
|
43
45
|
)
|
|
44
|
-
sdk.delegate = self
|
|
45
46
|
self.shortKit = sdk
|
|
46
47
|
|
|
47
48
|
ShortKitBridge.shared = self
|
|
48
49
|
|
|
49
50
|
subscribeToPublishers(sdk.player)
|
|
51
|
+
sdk.delegate = self
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// MARK: - Teardown
|
|
@@ -141,31 +143,33 @@ import ShortKit
|
|
|
141
143
|
shortKit?.player.setMaxBitrate(Int(bitrate))
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
// MARK: -
|
|
146
|
+
// MARK: - Custom Feed
|
|
145
147
|
|
|
146
|
-
public func
|
|
147
|
-
let
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
body = ["code": "network_error", "message": underlying.localizedDescription]
|
|
151
|
-
case .playbackError(let code, let message):
|
|
152
|
-
body = ["code": code, "message": message]
|
|
153
|
-
case .authError:
|
|
154
|
-
body = ["code": "auth_error", "message": "Invalid API key"]
|
|
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)
|
|
155
152
|
}
|
|
156
|
-
emitOnMain("onError", body: body)
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
public func
|
|
160
|
-
|
|
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
|
+
}
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
public func
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
// MARK: - Combine Subscriptions
|
|
@@ -305,6 +309,14 @@ import ShortKit
|
|
|
305
309
|
self?.emit("onPrefetchedAheadCountChanged", body: ["count": count])
|
|
306
310
|
}
|
|
307
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)
|
|
308
320
|
}
|
|
309
321
|
|
|
310
322
|
// MARK: - Event Emission Helpers
|
|
@@ -335,31 +347,6 @@ import ShortKit
|
|
|
335
347
|
emitOnMain(name, body: body)
|
|
336
348
|
}
|
|
337
349
|
|
|
338
|
-
/// Emit an article tap event.
|
|
339
|
-
public func emitArticleTapped(_ item: ContentItem) {
|
|
340
|
-
emitOnMain("onArticleTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/// Emit a comment tap event.
|
|
344
|
-
public func emitCommentTapped(_ item: ContentItem) {
|
|
345
|
-
emitOnMain("onCommentTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/// Emit an overlay share tap event.
|
|
349
|
-
public func emitOverlayShareTapped(_ item: ContentItem) {
|
|
350
|
-
emitOnMain("onOverlayShareTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/// Emit a save tap event.
|
|
354
|
-
public func emitSaveTapped(_ item: ContentItem) {
|
|
355
|
-
emitOnMain("onSaveTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/// Emit a like tap event.
|
|
359
|
-
public func emitLikeTapped(_ item: ContentItem) {
|
|
360
|
-
emitOnMain("onLikeTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
361
|
-
}
|
|
362
|
-
|
|
363
350
|
// MARK: - Content Item Serialization
|
|
364
351
|
|
|
365
352
|
/// Serialize a ContentItem to a JSON string for bridge transport.
|
|
@@ -382,6 +369,10 @@ import ShortKit
|
|
|
382
369
|
"thumbnailUrl": item.thumbnailUrl,
|
|
383
370
|
]
|
|
384
371
|
|
|
372
|
+
if let playbackId = item.playbackId {
|
|
373
|
+
dict["playbackId"] = playbackId
|
|
374
|
+
}
|
|
375
|
+
|
|
385
376
|
if let description = item.description {
|
|
386
377
|
dict["description"] = description
|
|
387
378
|
}
|
|
@@ -463,13 +454,17 @@ import ShortKit
|
|
|
463
454
|
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
464
455
|
let videoOverlay = parseVideoOverlay(obj["overlay"] as? String)
|
|
465
456
|
|
|
457
|
+
let feedSourceStr = obj["feedSource"] as? String ?? "algorithmic"
|
|
458
|
+
let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
|
|
459
|
+
|
|
466
460
|
return FeedConfig(
|
|
467
461
|
feedHeight: feedHeight,
|
|
468
462
|
videoOverlay: videoOverlay,
|
|
469
463
|
carouselOverlay: .none,
|
|
470
464
|
surveyOverlay: .none,
|
|
471
465
|
adOverlay: .none,
|
|
472
|
-
muteOnStart: muteOnStart
|
|
466
|
+
muteOnStart: muteOnStart,
|
|
467
|
+
feedSource: feedSource
|
|
473
468
|
)
|
|
474
469
|
}
|
|
475
470
|
|
|
@@ -525,6 +520,34 @@ import ShortKit
|
|
|
525
520
|
}
|
|
526
521
|
}
|
|
527
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
|
+
|
|
528
551
|
/// Parse optional custom dimensions JSON string into dictionary.
|
|
529
552
|
private static func parseCustomDimensions(_ json: String?) -> [String: String]? {
|
|
530
553
|
guard let json,
|
|
@@ -535,3 +558,14 @@ import ShortKit
|
|
|
535
558
|
return dict
|
|
536
559
|
}
|
|
537
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
|
+
}
|
|
@@ -31,8 +31,10 @@ import ShortKit
|
|
|
31
31
|
private weak var currentOverlayView: UIView?
|
|
32
32
|
/// Overlay for the upcoming cell (nativeID="overlay-next").
|
|
33
33
|
private weak var nextOverlayView: UIView?
|
|
34
|
-
/// The page index
|
|
34
|
+
/// The page index used for overlay transform calculations.
|
|
35
35
|
private var currentPage: Int = 0
|
|
36
|
+
/// Deferred page update to avoid flashing stale metadata.
|
|
37
|
+
private var pageUpdateWorkItem: DispatchWorkItem?
|
|
36
38
|
|
|
37
39
|
// MARK: - Lifecycle
|
|
38
40
|
|
|
@@ -86,6 +88,8 @@ import ShortKit
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
private func removeFeedViewController() {
|
|
91
|
+
pageUpdateWorkItem?.cancel()
|
|
92
|
+
pageUpdateWorkItem = nil
|
|
89
93
|
scrollObservation?.invalidate()
|
|
90
94
|
scrollObservation = nil
|
|
91
95
|
currentOverlayView?.transform = .identity
|
|
@@ -106,7 +110,6 @@ import ShortKit
|
|
|
106
110
|
|
|
107
111
|
private func setupScrollTracking(_ feedVC: ShortKitFeedViewController) {
|
|
108
112
|
guard let scrollView = findScrollView(in: feedVC.view) else {
|
|
109
|
-
NSLog("[ShortKitFeedView] Could not find scroll view in feed VC hierarchy.")
|
|
110
113
|
return
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -120,14 +123,47 @@ import ShortKit
|
|
|
120
123
|
guard cellHeight > 0 else { return }
|
|
121
124
|
|
|
122
125
|
let offset = scrollView.contentOffset.y
|
|
123
|
-
let delta = offset - CGFloat(currentPage) * cellHeight
|
|
124
126
|
|
|
125
|
-
//
|
|
127
|
+
// Detect page change, but DEFER updating currentPage.
|
|
128
|
+
//
|
|
129
|
+
// Why: when the scroll settles on a new page, overlay-current still
|
|
130
|
+
// shows the OLD page's metadata (React hasn't processed OVERLAY_ACTIVATE
|
|
131
|
+
// yet). If we update currentPage immediately, delta snaps to 0 and
|
|
132
|
+
// overlay-current becomes visible with stale data.
|
|
133
|
+
//
|
|
134
|
+
// By deferring ~80ms, overlay-next (which already shows the correct
|
|
135
|
+
// data via NextOverlayProvider) stays visible at y=0 while React
|
|
136
|
+
// processes the state update. After the delay, overlay-current has
|
|
137
|
+
// been re-rendered with correct data and takes over seamlessly.
|
|
126
138
|
let nearestPage = Int(round(offset / cellHeight))
|
|
127
139
|
if abs(offset - CGFloat(nearestPage) * cellHeight) < 1.0 {
|
|
140
|
+
if nearestPage != currentPage && pageUpdateWorkItem == nil {
|
|
141
|
+
let targetPage = nearestPage
|
|
142
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
143
|
+
guard let self else { return }
|
|
144
|
+
self.currentPage = targetPage
|
|
145
|
+
self.pageUpdateWorkItem = nil
|
|
146
|
+
// Reapply overlay transforms now that currentPage is updated.
|
|
147
|
+
// Without this, overlay-next (static NextOverlayProvider state)
|
|
148
|
+
// stays visible at y=0 while overlay-current (live state) stays
|
|
149
|
+
// hidden — no scroll event fires to trigger handleScrollOffset.
|
|
150
|
+
let h = self.bounds.height
|
|
151
|
+
self.currentOverlayView?.transform = .identity
|
|
152
|
+
self.nextOverlayView?.transform = CGAffineTransform(translationX: 0, y: h)
|
|
153
|
+
}
|
|
154
|
+
pageUpdateWorkItem = workItem
|
|
155
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08, execute: workItem)
|
|
156
|
+
}
|
|
157
|
+
} else if let workItem = pageUpdateWorkItem {
|
|
158
|
+
// User is scrolling again — apply pending update immediately
|
|
159
|
+
// so transforms stay aligned for the new gesture.
|
|
160
|
+
workItem.cancel()
|
|
161
|
+
pageUpdateWorkItem = nil
|
|
128
162
|
currentPage = nearestPage
|
|
129
163
|
}
|
|
130
164
|
|
|
165
|
+
let delta = offset - CGFloat(currentPage) * cellHeight
|
|
166
|
+
|
|
131
167
|
// Find the overlay views if not cached
|
|
132
168
|
if currentOverlayView == nil || nextOverlayView == nil {
|
|
133
169
|
findOverlayViews()
|
|
@@ -146,28 +182,39 @@ import ShortKit
|
|
|
146
182
|
}
|
|
147
183
|
}
|
|
148
184
|
|
|
149
|
-
/// Find the sibling RN overlay views by nativeID
|
|
150
|
-
///
|
|
151
|
-
///
|
|
152
|
-
///
|
|
185
|
+
/// Find the sibling RN overlay views by nativeID.
|
|
186
|
+
///
|
|
187
|
+
/// In Fabric interop, `ShortKitFeedView` (a Paper-style `RCTViewManager`
|
|
188
|
+
/// view) is wrapped in an intermediate `RCTViewComponentView`. So
|
|
189
|
+
/// `self.superview` is that wrapper — not the React `<View>` container
|
|
190
|
+
/// rendered by ShortKitFeed.tsx. We walk up the ancestor chain until we
|
|
191
|
+
/// find the overlays.
|
|
153
192
|
private func findOverlayViews() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
currentOverlayView
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
nextOverlayView
|
|
193
|
+
var ancestor: UIView? = superview
|
|
194
|
+
while let container = ancestor {
|
|
195
|
+
for child in container.subviews {
|
|
196
|
+
// Skip subtrees that contain ourselves
|
|
197
|
+
guard !self.isDescendant(of: child) else { continue }
|
|
198
|
+
|
|
199
|
+
if currentOverlayView == nil {
|
|
200
|
+
currentOverlayView = findView(withNativeID: "overlay-current", in: child)
|
|
201
|
+
}
|
|
202
|
+
if nextOverlayView == nil {
|
|
203
|
+
nextOverlayView = findView(withNativeID: "overlay-next", in: child)
|
|
204
|
+
}
|
|
164
205
|
}
|
|
206
|
+
if currentOverlayView != nil && nextOverlayView != nil { return }
|
|
207
|
+
ancestor = container.superview
|
|
165
208
|
}
|
|
166
209
|
}
|
|
167
210
|
|
|
168
|
-
/// Recursively find a view by its
|
|
211
|
+
/// Recursively find a view by its React Native `nativeID` prop.
|
|
212
|
+
/// In Fabric, this is stored on `RCTViewComponentView.nativeId`,
|
|
213
|
+
/// not `accessibilityIdentifier`.
|
|
169
214
|
private func findView(withNativeID nativeID: String, in view: UIView) -> UIView? {
|
|
170
|
-
if view.
|
|
215
|
+
if view.responds(to: Selector(("nativeId"))),
|
|
216
|
+
let rnNativeId = view.value(forKey: "nativeId") as? String,
|
|
217
|
+
rnNativeId == nativeID {
|
|
171
218
|
return view
|
|
172
219
|
}
|
|
173
220
|
for subview in view.subviews {
|
package/ios/ShortKitModule.mm
CHANGED
|
@@ -54,14 +54,10 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
54
54
|
@"onFeedTransition",
|
|
55
55
|
@"onFormatChange",
|
|
56
56
|
@"onPrefetchedAheadCountChanged",
|
|
57
|
+
@"onRemainingContentCountChanged",
|
|
57
58
|
@"onError",
|
|
58
59
|
@"onShareTapped",
|
|
59
60
|
@"onSurveyResponse",
|
|
60
|
-
@"onArticleTapped",
|
|
61
|
-
@"onCommentTapped",
|
|
62
|
-
@"onOverlayShareTapped",
|
|
63
|
-
@"onSaveTapped",
|
|
64
|
-
@"onLikeTapped",
|
|
65
61
|
@"onOverlayConfigure",
|
|
66
62
|
@"onOverlayActivate",
|
|
67
63
|
@"onOverlayReset",
|
|
@@ -69,6 +65,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
69
65
|
@"onOverlayRestore",
|
|
70
66
|
@"onOverlayTap",
|
|
71
67
|
@"onOverlayDoubleTap",
|
|
68
|
+
@"onContentTapped",
|
|
72
69
|
];
|
|
73
70
|
}
|
|
74
71
|
|
|
@@ -107,6 +104,7 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
107
104
|
|
|
108
105
|
RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
109
106
|
config:(NSString *)config
|
|
107
|
+
embedId:(NSString *)embedId
|
|
110
108
|
clientAppName:(NSString *)clientAppName
|
|
111
109
|
clientAppVersion:(NSString *)clientAppVersion
|
|
112
110
|
customDimensions:(NSString *)customDimensions) {
|
|
@@ -115,6 +113,7 @@ RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
|
|
|
115
113
|
|
|
116
114
|
_shortKitBridge = [[ShortKitBridge alloc] initWithApiKey:apiKey
|
|
117
115
|
config:config
|
|
116
|
+
embedId:embedId
|
|
118
117
|
clientAppName:clientAppName
|
|
119
118
|
clientAppVersion:clientAppVersion
|
|
120
119
|
customDimensions:customDimensions
|
|
@@ -192,6 +191,24 @@ RCT_EXPORT_METHOD(setMaxBitrate:(double)bitrate) {
|
|
|
192
191
|
[_shortKitBridge setMaxBitrate:bitrate];
|
|
193
192
|
}
|
|
194
193
|
|
|
194
|
+
// MARK: - Custom Feed
|
|
195
|
+
|
|
196
|
+
RCT_EXPORT_METHOD(setFeedItems:(NSString *)items) {
|
|
197
|
+
[_shortKitBridge setFeedItems:items];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
RCT_EXPORT_METHOD(appendFeedItems:(NSString *)items) {
|
|
201
|
+
[_shortKitBridge appendFeedItems:items];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
RCT_EXPORT_METHOD(fetchContent:(NSInteger)limit
|
|
205
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
206
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
207
|
+
[_shortKitBridge fetchContent:limit completion:^(NSString *json) {
|
|
208
|
+
resolve(json);
|
|
209
|
+
}];
|
|
210
|
+
}
|
|
211
|
+
|
|
195
212
|
// MARK: - New Architecture (TurboModule)
|
|
196
213
|
|
|
197
214
|
#ifdef RCT_NEW_ARCH_ENABLED
|