@shortkitsdk/react-native 0.1.0 → 0.2.0
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 +1 -105
- package/ios/ShortKitBridge.swift +1 -54
- package/ios/ShortKitFeedView.swift +67 -20
- package/ios/ShortKitModule.mm +0 -5
- package/package.json +1 -1
- package/src/ShortKitFeed.tsx +0 -55
- package/src/ShortKitProvider.tsx +7 -4
- package/src/specs/NativeShortKitModule.ts +0 -25
- package/src/types.ts +0 -5
|
@@ -2,6 +2,8 @@ package com.shortkit.reactnative
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.content.ContextWrapper
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
5
7
|
import android.util.Log
|
|
6
8
|
import android.view.View
|
|
7
9
|
import android.view.ViewGroup
|
|
@@ -36,6 +38,7 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
36
38
|
|
|
37
39
|
private var feedFragment: ShortKitFeedFragment? = null
|
|
38
40
|
private var fragmentContainerId: Int = View.generateViewId()
|
|
41
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
39
42
|
|
|
40
43
|
init {
|
|
41
44
|
id = fragmentContainerId
|
|
@@ -54,9 +57,12 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
54
57
|
/** Overlay for the upcoming cell (nativeID="overlay-next"). */
|
|
55
58
|
private var nextOverlayView: View? = null
|
|
56
59
|
|
|
57
|
-
/** The page index
|
|
60
|
+
/** The page index used for overlay transform calculations. */
|
|
58
61
|
private var currentPage: Int = 0
|
|
59
62
|
|
|
63
|
+
/** Deferred page update to avoid flashing stale metadata. */
|
|
64
|
+
private var pageUpdateRunnable: Runnable? = null
|
|
65
|
+
|
|
60
66
|
// -----------------------------------------------------------------------
|
|
61
67
|
// Lifecycle
|
|
62
68
|
// -----------------------------------------------------------------------
|
|
@@ -145,6 +151,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
private fun teardownScrollTracking() {
|
|
154
|
+
pageUpdateRunnable?.let { handler.removeCallbacks(it) }
|
|
155
|
+
pageUpdateRunnable = null
|
|
148
156
|
pageChangeCallback?.let { viewPager?.unregisterOnPageChangeCallback(it) }
|
|
149
157
|
pageChangeCallback = null
|
|
150
158
|
viewPager = null
|
|
@@ -162,6 +170,41 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
162
170
|
val cellHeight = height.toFloat()
|
|
163
171
|
if (cellHeight <= 0) return
|
|
164
172
|
|
|
173
|
+
// Detect page change, but DEFER updating currentPage.
|
|
174
|
+
//
|
|
175
|
+
// Why: when the scroll settles on a new page, overlay-current still
|
|
176
|
+
// shows the OLD page's metadata (React hasn't processed OVERLAY_ACTIVATE
|
|
177
|
+
// yet). If we update currentPage immediately, delta snaps to 0 and
|
|
178
|
+
// overlay-current becomes visible with stale data.
|
|
179
|
+
//
|
|
180
|
+
// By deferring ~80ms, overlay-next (which already shows the correct
|
|
181
|
+
// data via NextOverlayProvider) stays visible at y=0 while React
|
|
182
|
+
// processes the state update.
|
|
183
|
+
if (positionOffset == 0f) {
|
|
184
|
+
if (position != currentPage && pageUpdateRunnable == null) {
|
|
185
|
+
val targetPage = position
|
|
186
|
+
val runnable = Runnable {
|
|
187
|
+
currentPage = targetPage
|
|
188
|
+
pageUpdateRunnable = null
|
|
189
|
+
// Reapply overlay transforms now that currentPage is updated.
|
|
190
|
+
// Without this, overlay-next (static NextOverlayProvider state)
|
|
191
|
+
// stays visible at y=0 while overlay-current (live state) stays
|
|
192
|
+
// hidden — no scroll event fires to trigger handleScrollOffset.
|
|
193
|
+
val h = height.toFloat()
|
|
194
|
+
currentOverlayView?.translationY = 0f
|
|
195
|
+
nextOverlayView?.translationY = h
|
|
196
|
+
}
|
|
197
|
+
pageUpdateRunnable = runnable
|
|
198
|
+
handler.postDelayed(runnable, 80)
|
|
199
|
+
}
|
|
200
|
+
} else if (pageUpdateRunnable != null) {
|
|
201
|
+
// User is scrolling again — apply pending update immediately
|
|
202
|
+
// so transforms stay aligned for the new gesture.
|
|
203
|
+
handler.removeCallbacks(pageUpdateRunnable!!)
|
|
204
|
+
pageUpdateRunnable = null
|
|
205
|
+
currentPage = position
|
|
206
|
+
}
|
|
207
|
+
|
|
165
208
|
// positionOffset is 0.0 when settled, approaches 1.0 during forward scroll.
|
|
166
209
|
// position is the index of the page currently filling most of the screen.
|
|
167
210
|
val delta: Float = if (position >= currentPage) {
|
|
@@ -172,11 +215,6 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
172
215
|
-(cellHeight - positionOffsetPixels.toFloat())
|
|
173
216
|
}
|
|
174
217
|
|
|
175
|
-
// Update currentPage when the scroll settles
|
|
176
|
-
if (positionOffset == 0f) {
|
|
177
|
-
currentPage = position
|
|
178
|
-
}
|
|
179
|
-
|
|
180
218
|
// Find the overlay views if not cached
|
|
181
219
|
if (currentOverlayView == null || nextOverlayView == null) {
|
|
182
220
|
findOverlayViews()
|
|
@@ -196,23 +234,40 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
/**
|
|
199
|
-
* Find the sibling RN overlay views by nativeID.
|
|
200
|
-
*
|
|
201
|
-
*
|
|
237
|
+
* Find the sibling RN overlay views by nativeID.
|
|
238
|
+
*
|
|
239
|
+
* In Fabric interop, this view may be wrapped in an intermediate
|
|
240
|
+
* ViewGroup, so `getParent()` may not be the React `<View>` container
|
|
241
|
+
* from ShortKitFeed.tsx. We walk up the ancestor chain until we find
|
|
242
|
+
* the overlays.
|
|
202
243
|
*/
|
|
203
244
|
private fun findOverlayViews() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
currentOverlayView
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
nextOverlayView
|
|
245
|
+
var ancestor = parent as? ViewGroup ?: return
|
|
246
|
+
while (true) {
|
|
247
|
+
for (i in 0 until ancestor.childCount) {
|
|
248
|
+
val child = ancestor.getChildAt(i)
|
|
249
|
+
if (child === this || isOwnAncestor(child)) continue
|
|
250
|
+
|
|
251
|
+
if (currentOverlayView == null) {
|
|
252
|
+
currentOverlayView = ReactFindViewUtil.findView(child, "overlay-current")
|
|
253
|
+
}
|
|
254
|
+
if (nextOverlayView == null) {
|
|
255
|
+
nextOverlayView = ReactFindViewUtil.findView(child, "overlay-next")
|
|
256
|
+
}
|
|
214
257
|
}
|
|
258
|
+
if (currentOverlayView != null && nextOverlayView != null) return
|
|
259
|
+
ancestor = ancestor.parent as? ViewGroup ?: return
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Check if the given view is an ancestor of this view. */
|
|
264
|
+
private fun isOwnAncestor(view: View): Boolean {
|
|
265
|
+
var current: ViewParent? = parent
|
|
266
|
+
while (current != null) {
|
|
267
|
+
if (current === view) return true
|
|
268
|
+
current = current.parent
|
|
215
269
|
}
|
|
270
|
+
return false
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
/** Recursively find the first [ViewPager2] in a view hierarchy. */
|
|
@@ -11,12 +11,8 @@ import com.shortkit.FeedConfig
|
|
|
11
11
|
import com.shortkit.FeedHeight
|
|
12
12
|
import com.shortkit.FeedTransitionPhase
|
|
13
13
|
import com.shortkit.JsonValue
|
|
14
|
-
import com.shortkit.OverlayActionDelegate
|
|
15
14
|
import com.shortkit.ShortKit
|
|
16
|
-
import com.shortkit.ShortKitDelegate
|
|
17
|
-
import com.shortkit.ShortKitError
|
|
18
15
|
import com.shortkit.ShortKitPlayer
|
|
19
|
-
import com.shortkit.SurveyOption
|
|
20
16
|
import com.shortkit.VideoOverlayMode
|
|
21
17
|
import com.shortkit.CarouselOverlayMode
|
|
22
18
|
import com.shortkit.SurveyOverlayMode
|
|
@@ -30,8 +26,7 @@ import org.json.JSONArray
|
|
|
30
26
|
import org.json.JSONObject
|
|
31
27
|
|
|
32
28
|
class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
33
|
-
NativeShortKitModuleSpec(reactContext)
|
|
34
|
-
ShortKitDelegate {
|
|
29
|
+
NativeShortKitModuleSpec(reactContext) {
|
|
35
30
|
|
|
36
31
|
companion object {
|
|
37
32
|
const val NAME = "ShortKitModule"
|
|
@@ -116,9 +111,6 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
116
111
|
clientAppVersion = clientAppVersion,
|
|
117
112
|
customDimensions = dims
|
|
118
113
|
)
|
|
119
|
-
sdk.delegate = this
|
|
120
|
-
sdk.overlayActionDelegate = overlayDelegate
|
|
121
|
-
|
|
122
114
|
this.shortKit = sdk
|
|
123
115
|
shared = this
|
|
124
116
|
|
|
@@ -219,102 +211,6 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
219
211
|
shortKit?.player?.setMaxBitrate(bitrate.toInt())
|
|
220
212
|
}
|
|
221
213
|
|
|
222
|
-
// -----------------------------------------------------------------------
|
|
223
|
-
// ShortKitDelegate
|
|
224
|
-
// -----------------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
override fun onError(error: ShortKitError) {
|
|
227
|
-
val params = Arguments.createMap().apply {
|
|
228
|
-
when (error) {
|
|
229
|
-
is ShortKitError.NetworkError -> {
|
|
230
|
-
putString("code", "network_error")
|
|
231
|
-
putString("message", error.cause?.localizedMessage ?: "Network error")
|
|
232
|
-
}
|
|
233
|
-
is ShortKitError.ApiError -> {
|
|
234
|
-
putString("code", "api_error")
|
|
235
|
-
putString("message", error.message ?: "API error")
|
|
236
|
-
}
|
|
237
|
-
is ShortKitError.PlayerError -> {
|
|
238
|
-
putString("code", error.code)
|
|
239
|
-
putString("message", error.message ?: "Player error")
|
|
240
|
-
}
|
|
241
|
-
is ShortKitError.ConfigError -> {
|
|
242
|
-
putString("code", "config_error")
|
|
243
|
-
putString("message", error.message ?: "Config error")
|
|
244
|
-
}
|
|
245
|
-
is ShortKitError.AuthError -> {
|
|
246
|
-
putString("code", "auth_error")
|
|
247
|
-
putString("message", "Invalid API key")
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
sendEvent("onError", params)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
override fun onShareTapped(item: ContentItem) {
|
|
255
|
-
val params = Arguments.createMap().apply {
|
|
256
|
-
putString("item", serializeContentItemToJSON(item))
|
|
257
|
-
}
|
|
258
|
-
sendEvent("onShareTapped", params)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
override fun onSurveyResponse(surveyId: String, option: SurveyOption) {
|
|
262
|
-
val params = Arguments.createMap().apply {
|
|
263
|
-
putString("surveyId", surveyId)
|
|
264
|
-
putString("optionId", option.id)
|
|
265
|
-
putString("optionText", option.text)
|
|
266
|
-
}
|
|
267
|
-
sendEvent("onSurveyResponse", params)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// We implement the optional delegate methods as no-ops; events are handled
|
|
271
|
-
// via StateFlow/SharedFlow subscriptions instead.
|
|
272
|
-
override fun onFeedReady() {}
|
|
273
|
-
override fun onContentChanged(item: ContentItem) {}
|
|
274
|
-
override fun onFeedEmpty() {}
|
|
275
|
-
|
|
276
|
-
// -----------------------------------------------------------------------
|
|
277
|
-
// OverlayActionDelegate (separate object to avoid signature clash with
|
|
278
|
-
// ShortKitDelegate.onShareTapped)
|
|
279
|
-
// -----------------------------------------------------------------------
|
|
280
|
-
|
|
281
|
-
private val overlayDelegate = object : OverlayActionDelegate {
|
|
282
|
-
override fun onReadMoreTapped(item: ContentItem) {
|
|
283
|
-
val params = Arguments.createMap().apply {
|
|
284
|
-
putString("item", serializeContentItemToJSON(item))
|
|
285
|
-
}
|
|
286
|
-
sendEvent("onArticleTapped", params)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
override fun onShareTapped(item: ContentItem) {
|
|
290
|
-
val params = Arguments.createMap().apply {
|
|
291
|
-
putString("item", serializeContentItemToJSON(item))
|
|
292
|
-
}
|
|
293
|
-
sendEvent("onOverlayShareTapped", params)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
override fun onCommentTapped(item: ContentItem) {
|
|
297
|
-
val params = Arguments.createMap().apply {
|
|
298
|
-
putString("item", serializeContentItemToJSON(item))
|
|
299
|
-
}
|
|
300
|
-
sendEvent("onCommentTapped", params)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
override fun onSaveTapped(item: ContentItem) {
|
|
304
|
-
val params = Arguments.createMap().apply {
|
|
305
|
-
putString("item", serializeContentItemToJSON(item))
|
|
306
|
-
}
|
|
307
|
-
sendEvent("onSaveTapped", params)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
override fun onLikeTapped(item: ContentItem) {
|
|
311
|
-
val params = Arguments.createMap().apply {
|
|
312
|
-
putString("item", serializeContentItemToJSON(item))
|
|
313
|
-
}
|
|
314
|
-
sendEvent("onLikeTapped", params)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
214
|
// -----------------------------------------------------------------------
|
|
319
215
|
// Overlay lifecycle events (called by Fabric view)
|
|
320
216
|
// -----------------------------------------------------------------------
|
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
|
|
|
@@ -41,7 +41,6 @@ import ShortKit
|
|
|
41
41
|
clientAppVersion: clientAppVersion,
|
|
42
42
|
customDimensions: dimensions
|
|
43
43
|
)
|
|
44
|
-
sdk.delegate = self
|
|
45
44
|
self.shortKit = sdk
|
|
46
45
|
|
|
47
46
|
ShortKitBridge.shared = self
|
|
@@ -141,33 +140,6 @@ import ShortKit
|
|
|
141
140
|
shortKit?.player.setMaxBitrate(Int(bitrate))
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
// MARK: - ShortKitDelegate
|
|
145
|
-
|
|
146
|
-
public func shortKit(_ shortKit: ShortKit, didEncounterError error: ShortKitError) {
|
|
147
|
-
let body: [String: Any]
|
|
148
|
-
switch error {
|
|
149
|
-
case .networkError(let underlying):
|
|
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"]
|
|
155
|
-
}
|
|
156
|
-
emitOnMain("onError", body: body)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
public func shortKit(_ shortKit: ShortKit, didTapShareFor item: ContentItem) {
|
|
160
|
-
emitOnMain("onShareTapped", body: ["item": serializeContentItemToJSON(item)])
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
public func shortKit(_ shortKit: ShortKit, didRespondToSurvey surveyId: String, with option: SurveyOption) {
|
|
164
|
-
emitOnMain("onSurveyResponse", body: [
|
|
165
|
-
"surveyId": surveyId,
|
|
166
|
-
"optionId": option.id,
|
|
167
|
-
"optionText": option.text
|
|
168
|
-
])
|
|
169
|
-
}
|
|
170
|
-
|
|
171
143
|
// MARK: - Combine Subscriptions
|
|
172
144
|
|
|
173
145
|
private func subscribeToPublishers(_ player: ShortKitPlayer) {
|
|
@@ -335,31 +307,6 @@ import ShortKit
|
|
|
335
307
|
emitOnMain(name, body: body)
|
|
336
308
|
}
|
|
337
309
|
|
|
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
310
|
// MARK: - Content Item Serialization
|
|
364
311
|
|
|
365
312
|
/// Serialize a ContentItem to a JSON string for bridge transport.
|
|
@@ -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
|
@@ -57,11 +57,6 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
57
57
|
@"onError",
|
|
58
58
|
@"onShareTapped",
|
|
59
59
|
@"onSurveyResponse",
|
|
60
|
-
@"onArticleTapped",
|
|
61
|
-
@"onCommentTapped",
|
|
62
|
-
@"onOverlayShareTapped",
|
|
63
|
-
@"onSaveTapped",
|
|
64
|
-
@"onLikeTapped",
|
|
65
60
|
@"onOverlayConfigure",
|
|
66
61
|
@"onOverlayActivate",
|
|
67
62
|
@"onOverlayReset",
|
package/package.json
CHANGED
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -29,11 +29,6 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
29
29
|
onLoop,
|
|
30
30
|
onFeedTransition,
|
|
31
31
|
onFormatChange,
|
|
32
|
-
onArticleTapped,
|
|
33
|
-
onCommentTapped,
|
|
34
|
-
onOverlayShareTapped,
|
|
35
|
-
onSaveTapped,
|
|
36
|
-
onLikeTapped,
|
|
37
32
|
} = props;
|
|
38
33
|
|
|
39
34
|
const context = useContext(ShortKitContext);
|
|
@@ -114,51 +109,6 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
114
109
|
);
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
if (onArticleTapped) {
|
|
118
|
-
subscriptions.push(
|
|
119
|
-
NativeShortKitModule.onArticleTapped((event) => {
|
|
120
|
-
const item = deserializeContentItem(event.item);
|
|
121
|
-
if (item) onArticleTapped(item);
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (onCommentTapped) {
|
|
127
|
-
subscriptions.push(
|
|
128
|
-
NativeShortKitModule.onCommentTapped((event) => {
|
|
129
|
-
const item = deserializeContentItem(event.item);
|
|
130
|
-
if (item) onCommentTapped(item);
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (onOverlayShareTapped) {
|
|
136
|
-
subscriptions.push(
|
|
137
|
-
NativeShortKitModule.onOverlayShareTapped((event) => {
|
|
138
|
-
const item = deserializeContentItem(event.item);
|
|
139
|
-
if (item) onOverlayShareTapped(item);
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (onSaveTapped) {
|
|
145
|
-
subscriptions.push(
|
|
146
|
-
NativeShortKitModule.onSaveTapped((event) => {
|
|
147
|
-
const item = deserializeContentItem(event.item);
|
|
148
|
-
if (item) onSaveTapped(item);
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (onLikeTapped) {
|
|
154
|
-
subscriptions.push(
|
|
155
|
-
NativeShortKitModule.onLikeTapped((event) => {
|
|
156
|
-
const item = deserializeContentItem(event.item);
|
|
157
|
-
if (item) onLikeTapped(item);
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
112
|
return () => {
|
|
163
113
|
for (const sub of subscriptions) {
|
|
164
114
|
sub.remove();
|
|
@@ -171,11 +121,6 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
171
121
|
onLoop,
|
|
172
122
|
onFeedTransition,
|
|
173
123
|
onFormatChange,
|
|
174
|
-
onArticleTapped,
|
|
175
|
-
onCommentTapped,
|
|
176
|
-
onOverlayShareTapped,
|
|
177
|
-
onSaveTapped,
|
|
178
|
-
onLikeTapped,
|
|
179
124
|
]);
|
|
180
125
|
|
|
181
126
|
// ---------------------------------------------------------------------------
|
package/src/ShortKitProvider.tsx
CHANGED
|
@@ -68,7 +68,7 @@ type Action =
|
|
|
68
68
|
| { type: 'CUE'; payload: { text: string; startTime: number; endTime: number } | null }
|
|
69
69
|
| { type: 'PREFETCH_COUNT'; payload: number }
|
|
70
70
|
| { type: 'OVERLAY_CONFIGURE'; payload: ContentItem }
|
|
71
|
-
| { type: 'OVERLAY_ACTIVATE' }
|
|
71
|
+
| { type: 'OVERLAY_ACTIVATE'; payload: ContentItem }
|
|
72
72
|
| { type: 'OVERLAY_RESET' }
|
|
73
73
|
| { type: 'OVERLAY_FADE_OUT' }
|
|
74
74
|
| { type: 'OVERLAY_RESTORE' }
|
|
@@ -111,7 +111,7 @@ function reducer(state: State, action: Action): State {
|
|
|
111
111
|
case 'OVERLAY_CONFIGURE':
|
|
112
112
|
return { ...state, nextItem: action.payload };
|
|
113
113
|
case 'OVERLAY_ACTIVATE':
|
|
114
|
-
return { ...state, currentItem:
|
|
114
|
+
return { ...state, currentItem: action.payload, isActive: true };
|
|
115
115
|
case 'OVERLAY_RESET':
|
|
116
116
|
// Don't set isActive = false — the overlay stays mounted during
|
|
117
117
|
// transitions. In the native SDK each cell has its own overlay
|
|
@@ -338,8 +338,11 @@ export function ShortKitProvider({
|
|
|
338
338
|
);
|
|
339
339
|
|
|
340
340
|
subscriptions.push(
|
|
341
|
-
NativeShortKitModule.onOverlayActivate((
|
|
342
|
-
|
|
341
|
+
NativeShortKitModule.onOverlayActivate((event) => {
|
|
342
|
+
const item = deserializeContentItem(event.item);
|
|
343
|
+
if (item) {
|
|
344
|
+
dispatch({ type: 'OVERLAY_ACTIVATE', payload: item });
|
|
345
|
+
}
|
|
343
346
|
}),
|
|
344
347
|
);
|
|
345
348
|
|
|
@@ -92,26 +92,6 @@ type SurveyResponseEvent = Readonly<{
|
|
|
92
92
|
optionText: string;
|
|
93
93
|
}>;
|
|
94
94
|
|
|
95
|
-
type ArticleTappedEvent = Readonly<{
|
|
96
|
-
item: string; // JSON-serialized ContentItem
|
|
97
|
-
}>;
|
|
98
|
-
|
|
99
|
-
type CommentTappedEvent = Readonly<{
|
|
100
|
-
item: string; // JSON-serialized ContentItem
|
|
101
|
-
}>;
|
|
102
|
-
|
|
103
|
-
type OverlayShareTappedEvent = Readonly<{
|
|
104
|
-
item: string; // JSON-serialized ContentItem
|
|
105
|
-
}>;
|
|
106
|
-
|
|
107
|
-
type SaveTappedEvent = Readonly<{
|
|
108
|
-
item: string; // JSON-serialized ContentItem
|
|
109
|
-
}>;
|
|
110
|
-
|
|
111
|
-
type LikeTappedEvent = Readonly<{
|
|
112
|
-
item: string; // JSON-serialized ContentItem
|
|
113
|
-
}>;
|
|
114
|
-
|
|
115
95
|
type OverlayConfigureEvent = Readonly<{
|
|
116
96
|
item: string; // JSON-serialized ContentItem
|
|
117
97
|
}>;
|
|
@@ -184,11 +164,6 @@ export interface Spec extends TurboModule {
|
|
|
184
164
|
readonly onError: EventEmitter<ErrorEvent>;
|
|
185
165
|
readonly onShareTapped: EventEmitter<ShareTappedEvent>;
|
|
186
166
|
readonly onSurveyResponse: EventEmitter<SurveyResponseEvent>;
|
|
187
|
-
readonly onArticleTapped: EventEmitter<ArticleTappedEvent>;
|
|
188
|
-
readonly onCommentTapped: EventEmitter<CommentTappedEvent>;
|
|
189
|
-
readonly onOverlayShareTapped: EventEmitter<OverlayShareTappedEvent>;
|
|
190
|
-
readonly onSaveTapped: EventEmitter<SaveTappedEvent>;
|
|
191
|
-
readonly onLikeTapped: EventEmitter<LikeTappedEvent>;
|
|
192
167
|
readonly onOverlayConfigure: EventEmitter<OverlayConfigureEvent>;
|
|
193
168
|
readonly onOverlayActivate: EventEmitter<OverlayActivateEvent>;
|
|
194
169
|
readonly onOverlayReset: EventEmitter<OverlayResetEvent>;
|
package/src/types.ts
CHANGED
|
@@ -127,11 +127,6 @@ export interface ShortKitFeedProps {
|
|
|
127
127
|
onLoop?: (event: LoopEvent) => void;
|
|
128
128
|
onFeedTransition?: (event: FeedTransitionEvent) => void;
|
|
129
129
|
onFormatChange?: (event: FormatChangeEvent) => void;
|
|
130
|
-
onArticleTapped?: (item: ContentItem) => void;
|
|
131
|
-
onCommentTapped?: (item: ContentItem) => void;
|
|
132
|
-
onOverlayShareTapped?: (item: ContentItem) => void;
|
|
133
|
-
onSaveTapped?: (item: ContentItem) => void;
|
|
134
|
-
onLikeTapped?: (item: ContentItem) => void;
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
// --- Hook Return Types ---
|