@shortkitsdk/react-native 0.2.6 → 0.2.12
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/ShortKitReactNative.podspec +1 -0
- package/android/build.gradle.kts +17 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +570 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -2
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
- package/ios/ReactCarouselOverlayHost.swift +177 -0
- package/ios/ReactLoadingHost.swift +38 -0
- package/ios/ReactOverlayHost.swift +444 -0
- package/ios/SKFabricSurfaceWrapper.h +18 -0
- package/ios/SKFabricSurfaceWrapper.mm +57 -0
- package/ios/ShortKitBridge.swift +220 -63
- package/ios/ShortKitFeedView.swift +82 -228
- package/ios/ShortKitFeedViewManager.mm +3 -2
- package/ios/ShortKitModule.mm +69 -37
- package/ios/ShortKitPlayerNativeView.swift +39 -8
- package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3683 -1249
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +56 -15
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3683 -1249
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +56 -15
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitWidgetNativeView.swift +3 -3
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
- package/src/ShortKitCommands.ts +31 -0
- package/src/ShortKitContext.ts +6 -24
- package/src/ShortKitFeed.tsx +124 -41
- package/src/ShortKitLoadingSurface.tsx +24 -0
- package/src/ShortKitOverlaySurface.tsx +313 -0
- package/src/ShortKitPlayer.tsx +30 -9
- package/src/ShortKitProvider.tsx +28 -285
- package/src/index.ts +9 -3
- package/src/serialization.ts +20 -39
- package/src/specs/NativeShortKitModule.ts +74 -45
- package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
- package/src/types.ts +84 -16
- package/src/useShortKit.ts +1 -3
- package/src/useShortKitPlayer.ts +7 -7
- package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
- package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
- package/ios/ShortKitOverlayBridge.swift +0 -111
- package/src/CarouselOverlayManager.tsx +0 -70
- package/src/OverlayManager.tsx +0 -87
- package/src/useShortKitCarousel.ts +0 -29
|
@@ -9,17 +9,14 @@ import android.view.View
|
|
|
9
9
|
import android.view.ViewGroup
|
|
10
10
|
import android.widget.FrameLayout
|
|
11
11
|
import androidx.fragment.app.FragmentActivity
|
|
12
|
-
import
|
|
13
|
-
import com.facebook.react.uimanager.util.ReactFindViewUtil
|
|
14
|
-
import com.shortkit.ShortKitFeedFragment
|
|
12
|
+
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* Fabric native view that embeds [ShortKitFeedFragment] using Android
|
|
18
16
|
* Fragment transactions. Props are set by [ShortKitFeedViewManager].
|
|
19
17
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* so they move with the active cell during swipe transitions.
|
|
18
|
+
* The native SDK owns overlay lifecycle via [ReactOverlayHost] and
|
|
19
|
+
* [ReactCarouselOverlayHost] — this view is purely a fragment container.
|
|
23
20
|
*
|
|
24
21
|
* Android equivalent of iOS `ShortKitFeedView.swift`.
|
|
25
22
|
*/
|
|
@@ -30,44 +27,92 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
30
27
|
// -----------------------------------------------------------------------
|
|
31
28
|
|
|
32
29
|
var config: String? = null
|
|
33
|
-
var
|
|
30
|
+
var feedId: String? = null
|
|
31
|
+
var startAtItemId: String? = null
|
|
32
|
+
var preloadId: String? = null
|
|
34
33
|
|
|
35
34
|
// -----------------------------------------------------------------------
|
|
36
35
|
// Fragment management
|
|
37
36
|
// -----------------------------------------------------------------------
|
|
38
37
|
|
|
39
38
|
private var feedFragment: ShortKitFeedFragment? = null
|
|
39
|
+
private val fragmentContainer: FrameLayout
|
|
40
40
|
private var fragmentContainerId: Int = View.generateViewId()
|
|
41
41
|
private val handler = Handler(Looper.getMainLooper())
|
|
42
42
|
|
|
43
|
+
// The React Native-assigned size. Stored so we can re-apply it after the
|
|
44
|
+
// SDK's forceLayoutIfNeeded potentially re-measures to full-screen size.
|
|
45
|
+
private var rnWidth: Int = 0
|
|
46
|
+
private var rnHeight: Int = 0
|
|
47
|
+
|
|
43
48
|
init {
|
|
44
|
-
|
|
49
|
+
// Use a child FrameLayout as the fragment container. Fabric overrides
|
|
50
|
+
// this view's own id, so we can't use it as the fragment container.
|
|
51
|
+
fragmentContainer = FrameLayout(context).apply {
|
|
52
|
+
id = fragmentContainerId
|
|
53
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
54
|
+
}
|
|
55
|
+
addView(fragmentContainer)
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
// -----------------------------------------------------------------------
|
|
48
|
-
//
|
|
59
|
+
// Fabric layout workarounds
|
|
49
60
|
// -----------------------------------------------------------------------
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Fabric intercepts requestLayout() and doesn't propagate measure/layout
|
|
64
|
+
* to programmatically-added children. Override to schedule a manual pass.
|
|
65
|
+
*/
|
|
66
|
+
override fun requestLayout() {
|
|
67
|
+
super.requestLayout()
|
|
68
|
+
@Suppress("UNNECESSARY_SAFE_CALL")
|
|
69
|
+
handler?.post(layoutRunnable)
|
|
70
|
+
}
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
private val layoutRunnable = Runnable {
|
|
73
|
+
measure(
|
|
74
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
75
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
76
|
+
)
|
|
77
|
+
layout(left, top, right, bottom)
|
|
78
|
+
}
|
|
62
79
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Re-apply the React Native-assigned size to the fragment container and
|
|
82
|
+
* all descendants (fragment root, VP, RV). The SDK's forceLayoutIfNeeded
|
|
83
|
+
* may have sized them to the full activity height instead.
|
|
84
|
+
*/
|
|
85
|
+
private val constrainRunnable = Runnable {
|
|
86
|
+
val w = rnWidth
|
|
87
|
+
val h = rnHeight
|
|
88
|
+
if (w == 0 || h == 0) return@Runnable
|
|
89
|
+
|
|
90
|
+
val fragView = feedFragment?.view ?: return@Runnable
|
|
91
|
+
val wSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY)
|
|
92
|
+
val hSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
|
|
93
|
+
|
|
94
|
+
// Re-pin container
|
|
95
|
+
fragmentContainer.measure(wSpec, hSpec)
|
|
96
|
+
fragmentContainer.layout(0, 0, w, h)
|
|
97
|
+
|
|
98
|
+
// Re-pin fragment root, VP, and RV if they got the wrong size
|
|
99
|
+
if (fragView.height != h) {
|
|
100
|
+
fragView.measure(wSpec, hSpec)
|
|
101
|
+
fragView.layout(0, 0, w, h)
|
|
102
|
+
}
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
val vp = findViewPager2(fragView)
|
|
105
|
+
if (vp != null && vp.height != h) {
|
|
106
|
+
vp.measure(wSpec, hSpec)
|
|
107
|
+
vp.layout(0, 0, w, h)
|
|
108
|
+
val rv = vp.getChildAt(0) as? ViewGroup
|
|
109
|
+
if (rv != null) {
|
|
110
|
+
rv.measure(wSpec, hSpec)
|
|
111
|
+
rv.layout(0, 0, w, h)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
68
115
|
|
|
69
|
-
/** Deferred page update to avoid flashing stale metadata. */
|
|
70
|
-
private var pageUpdateRunnable: Runnable? = null
|
|
71
116
|
|
|
72
117
|
// -----------------------------------------------------------------------
|
|
73
118
|
// Lifecycle
|
|
@@ -75,12 +120,21 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
75
120
|
|
|
76
121
|
override fun onAttachedToWindow() {
|
|
77
122
|
super.onAttachedToWindow()
|
|
78
|
-
|
|
123
|
+
// Defer to the next message-loop tick. onAttachedToWindow can fire
|
|
124
|
+
// during a parent fragment transaction (e.g. react-native-screens
|
|
125
|
+
// ScreenStack.onUpdate), and FragmentManager forbids nested
|
|
126
|
+
// commitNow calls. Posting ensures we run after the outer
|
|
127
|
+
// transaction completes — still before the first draw pass.
|
|
128
|
+
handler.post {
|
|
129
|
+
if (isAttachedToWindow) embedFeedFragmentIfNeeded()
|
|
130
|
+
}
|
|
79
131
|
}
|
|
80
132
|
|
|
81
133
|
override fun onDetachedFromWindow() {
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
135
|
+
ShortKitBridge.staticPendingFeedViews.remove(this)
|
|
136
|
+
}
|
|
137
|
+
suspendFeedFragment()
|
|
84
138
|
super.onDetachedFromWindow()
|
|
85
139
|
}
|
|
86
140
|
|
|
@@ -88,233 +142,177 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
88
142
|
// Fragment containment
|
|
89
143
|
// -----------------------------------------------------------------------
|
|
90
144
|
|
|
91
|
-
|
|
92
|
-
|
|
145
|
+
internal fun embedFeedFragmentIfNeeded() {
|
|
146
|
+
val activity = getReactActivity()
|
|
147
|
+
if (activity == null) {
|
|
148
|
+
Log.e(TAG, "embedFeedFragment: FragmentActivity not found in context chain")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
93
151
|
|
|
94
|
-
|
|
95
|
-
|
|
152
|
+
// Show a hidden fragment (e.g. returning to this tab).
|
|
153
|
+
// Using hide()/show() instead of detach()/attach() preserves the
|
|
154
|
+
// entire view hierarchy (ViewPager, RecyclerView, player surface)
|
|
155
|
+
// so there is zero flicker — matching iOS behavior exactly.
|
|
156
|
+
val existingFragment = feedFragment
|
|
157
|
+
if (existingFragment != null) {
|
|
158
|
+
if (existingFragment.isHidden) {
|
|
159
|
+
try {
|
|
160
|
+
activity.supportFragmentManager
|
|
161
|
+
.beginTransaction()
|
|
162
|
+
.show(existingFragment)
|
|
163
|
+
.commitNowAllowingStateLoss()
|
|
164
|
+
existingFragment.activate()
|
|
165
|
+
} catch (e: Exception) {
|
|
166
|
+
Log.e(TAG, "Failed to show feed fragment", e)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
feedId?.let { id ->
|
|
170
|
+
ShortKitBridge.shared?.registerFeed(id)
|
|
171
|
+
ShortKitBridge.shared?.registerFeedFragment(id, existingFragment)
|
|
172
|
+
}
|
|
96
173
|
return
|
|
97
174
|
}
|
|
98
175
|
|
|
99
|
-
val
|
|
100
|
-
|
|
176
|
+
val sdk = ShortKitBridge.shared?.sdk ?: run {
|
|
177
|
+
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
178
|
+
ShortKitBridge.staticPendingFeedViews.add(this)
|
|
179
|
+
}
|
|
101
180
|
return
|
|
102
181
|
}
|
|
103
182
|
|
|
104
|
-
|
|
183
|
+
var feedConfig = ShortKitBridge.parseFeedConfig(config ?: "{}", context)
|
|
184
|
+
|
|
185
|
+
preloadId?.let { id ->
|
|
186
|
+
val preload = ShortKitBridge.shared?.consumePreload(id)
|
|
187
|
+
if (preload != null) {
|
|
188
|
+
feedConfig = feedConfig.copy(preload = preload)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
val fragment = ShortKitFeedFragment.newInstance(sdk, feedConfig, startAtItemId)
|
|
105
193
|
|
|
106
194
|
try {
|
|
107
195
|
activity.supportFragmentManager
|
|
108
196
|
.beginTransaction()
|
|
109
197
|
.replace(fragmentContainerId, fragment)
|
|
110
198
|
.commitNowAllowingStateLoss()
|
|
111
|
-
this.feedFragment = fragment
|
|
112
|
-
|
|
199
|
+
this.feedFragment = fragment
|
|
200
|
+
|
|
201
|
+
// View.generateViewId() can return IDs that collide with Fabric-
|
|
202
|
+
// managed views (e.g. ReactSurfaceView also gets id=1). When that
|
|
203
|
+
// happens, FragmentManager.replace() places the fragment view in
|
|
204
|
+
// the wrong parent. Detect and correct this by reparenting.
|
|
205
|
+
val fragView = fragment.view
|
|
206
|
+
val fragParent = fragView?.parent
|
|
207
|
+
if (fragParent != null && fragParent !== fragmentContainer) {
|
|
208
|
+
Log.w(TAG, "embedFeedFragment: reparenting — fragment placed in ${fragParent.javaClass.simpleName} due to ID collision")
|
|
209
|
+
(fragParent as? ViewGroup)?.removeView(fragView)
|
|
210
|
+
fragmentContainer.addView(fragView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Pin the fragment container to the React Native-assigned size.
|
|
214
|
+
// Without this, MATCH_PARENT causes Android to re-measure the
|
|
215
|
+
// container to the full activity size, and the SDK's deferred
|
|
216
|
+
// layout forcing picks up the wrong (full-screen) dimensions.
|
|
217
|
+
rnWidth = measuredWidth
|
|
218
|
+
rnHeight = measuredHeight
|
|
219
|
+
fragmentContainer.layoutParams = LayoutParams(measuredWidth, measuredHeight)
|
|
220
|
+
fragmentContainer.measure(
|
|
221
|
+
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
222
|
+
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
|
223
|
+
)
|
|
224
|
+
fragmentContainer.layout(0, 0, measuredWidth, measuredHeight)
|
|
225
|
+
// Schedule a delayed re-pin: the SDK's forceLayoutIfNeeded will
|
|
226
|
+
// run when deferCellSetup exhausts retries (~3 post ticks later)
|
|
227
|
+
// and may size the VP/RV to the wrong dimensions. Re-apply our
|
|
228
|
+
// correct RN size after it runs.
|
|
229
|
+
handler.postDelayed(constrainRunnable, 500)
|
|
230
|
+
|
|
231
|
+
if (measuredWidth == 0 || measuredHeight == 0) {
|
|
232
|
+
Log.w(TAG, "embedFeedFragment: WARNING — container is 0x0, overlays will be invisible. Check if parent has display:none or flex:0")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
feedId?.let { id ->
|
|
236
|
+
ShortKitBridge.shared?.registerFeed(id)
|
|
237
|
+
ShortKitBridge.shared?.registerFeedFragment(id, fragment)
|
|
238
|
+
}
|
|
113
239
|
} catch (e: Exception) {
|
|
114
240
|
Log.e(TAG, "Failed to embed feed fragment", e)
|
|
115
241
|
}
|
|
116
242
|
}
|
|
117
243
|
|
|
118
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Hide the fragment without destroying its view hierarchy (mirrors iOS
|
|
246
|
+
* `suspendFeedViewController`). Using hide() instead of detach() keeps
|
|
247
|
+
* the ViewPager, RecyclerView, player surface, and all overlay views
|
|
248
|
+
* alive in memory — so returning to the tab is instant with no flicker.
|
|
249
|
+
*/
|
|
250
|
+
private fun suspendFeedFragment() {
|
|
251
|
+
feedId?.let { id ->
|
|
252
|
+
ShortKitBridge.shared?.unregisterFeed(id)
|
|
253
|
+
ShortKitBridge.shared?.unregisterFeedFragment(id)
|
|
254
|
+
}
|
|
119
255
|
val fragment = feedFragment ?: return
|
|
120
|
-
feedFragment = null // Clear immediately to prevent leaks on early return
|
|
121
256
|
|
|
122
257
|
try {
|
|
258
|
+
fragment.deactivate()
|
|
123
259
|
val activity = getReactActivity() ?: return
|
|
124
260
|
activity.supportFragmentManager
|
|
125
261
|
.beginTransaction()
|
|
126
|
-
.
|
|
262
|
+
.hide(fragment)
|
|
127
263
|
.commitNowAllowingStateLoss()
|
|
128
264
|
} catch (e: IllegalStateException) {
|
|
129
|
-
// Expected during Activity teardown —
|
|
265
|
+
// Expected during Activity teardown — fall back to full destroy
|
|
266
|
+
feedFragment = null
|
|
130
267
|
} catch (e: Exception) {
|
|
131
|
-
Log.e(TAG, "Unexpected error
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// -----------------------------------------------------------------------
|
|
136
|
-
// Scroll tracking
|
|
137
|
-
// -----------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
private fun setupScrollTracking(fragment: ShortKitFeedFragment) {
|
|
140
|
-
val pager = findViewPager2(fragment.view ?: return) ?: run {
|
|
141
|
-
Log.w(TAG, "Could not find ViewPager2 in feed fragment hierarchy.")
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
viewPager = pager
|
|
145
|
-
|
|
146
|
-
val callback = object : ViewPager2.OnPageChangeCallback() {
|
|
147
|
-
override fun onPageScrolled(
|
|
148
|
-
position: Int,
|
|
149
|
-
positionOffset: Float,
|
|
150
|
-
positionOffsetPixels: Int
|
|
151
|
-
) {
|
|
152
|
-
handleScrollOffset(position, positionOffset, positionOffsetPixels)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
pageChangeCallback = callback
|
|
156
|
-
pager.registerOnPageChangeCallback(callback)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private fun teardownScrollTracking() {
|
|
160
|
-
pageUpdateRunnable?.let { handler.removeCallbacks(it) }
|
|
161
|
-
pageUpdateRunnable = null
|
|
162
|
-
pageChangeCallback?.let { viewPager?.unregisterOnPageChangeCallback(it) }
|
|
163
|
-
pageChangeCallback = null
|
|
164
|
-
viewPager = null
|
|
165
|
-
currentOverlayView?.translationY = 0f
|
|
166
|
-
nextOverlayView?.translationY = 0f
|
|
167
|
-
currentCarouselOverlayView?.translationY = 0f
|
|
168
|
-
nextCarouselOverlayView?.translationY = 0f
|
|
169
|
-
currentOverlayView = null
|
|
170
|
-
nextOverlayView = null
|
|
171
|
-
currentCarouselOverlayView = null
|
|
172
|
-
nextCarouselOverlayView = null
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private fun handleScrollOffset(
|
|
176
|
-
position: Int,
|
|
177
|
-
positionOffset: Float,
|
|
178
|
-
positionOffsetPixels: Int
|
|
179
|
-
) {
|
|
180
|
-
val cellHeight = height.toFloat()
|
|
181
|
-
if (cellHeight <= 0) return
|
|
182
|
-
|
|
183
|
-
// Detect page change, but DEFER updating currentPage.
|
|
184
|
-
//
|
|
185
|
-
// Why: when the scroll settles on a new page, overlay-current still
|
|
186
|
-
// shows the OLD page's metadata (React hasn't processed OVERLAY_ACTIVATE
|
|
187
|
-
// yet). If we update currentPage immediately, delta snaps to 0 and
|
|
188
|
-
// overlay-current becomes visible with stale data.
|
|
189
|
-
//
|
|
190
|
-
// By deferring ~80ms, overlay-next (which already shows the correct
|
|
191
|
-
// data via NextOverlayProvider) stays visible at y=0 while React
|
|
192
|
-
// processes the state update.
|
|
193
|
-
if (positionOffset == 0f) {
|
|
194
|
-
if (position != currentPage && pageUpdateRunnable == null) {
|
|
195
|
-
val targetPage = position
|
|
196
|
-
val runnable = Runnable {
|
|
197
|
-
currentPage = targetPage
|
|
198
|
-
pageUpdateRunnable = null
|
|
199
|
-
// Reapply overlay transforms now that currentPage is updated.
|
|
200
|
-
// Without this, overlay-next (static NextOverlayProvider state)
|
|
201
|
-
// stays visible at y=0 while overlay-current (live state) stays
|
|
202
|
-
// hidden — no scroll event fires to trigger handleScrollOffset.
|
|
203
|
-
val h = height.toFloat()
|
|
204
|
-
currentOverlayView?.translationY = 0f
|
|
205
|
-
nextOverlayView?.translationY = h
|
|
206
|
-
currentCarouselOverlayView?.translationY = 0f
|
|
207
|
-
nextCarouselOverlayView?.translationY = h
|
|
208
|
-
}
|
|
209
|
-
pageUpdateRunnable = runnable
|
|
210
|
-
handler.postDelayed(runnable, 80)
|
|
211
|
-
}
|
|
212
|
-
} else if (pageUpdateRunnable != null) {
|
|
213
|
-
// User is scrolling again — apply pending update immediately
|
|
214
|
-
// so transforms stay aligned for the new gesture.
|
|
215
|
-
handler.removeCallbacks(pageUpdateRunnable!!)
|
|
216
|
-
pageUpdateRunnable = null
|
|
217
|
-
currentPage = position
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// positionOffset is 0.0 when settled, approaches 1.0 during forward scroll.
|
|
221
|
-
// position is the index of the page currently filling most of the screen.
|
|
222
|
-
val delta: Float = if (position >= currentPage) {
|
|
223
|
-
// Forward scroll (or idle)
|
|
224
|
-
positionOffsetPixels.toFloat()
|
|
225
|
-
} else {
|
|
226
|
-
// Backward scroll: position dropped below currentPage
|
|
227
|
-
-(cellHeight - positionOffsetPixels.toFloat())
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Find the overlay views if not cached
|
|
231
|
-
if (currentOverlayView == null || nextOverlayView == null) {
|
|
232
|
-
findOverlayViews()
|
|
233
|
-
}
|
|
234
|
-
if (currentCarouselOverlayView == null || nextCarouselOverlayView == null) {
|
|
235
|
-
findCarouselOverlayViews()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Current overlays follow the active cell
|
|
239
|
-
currentOverlayView?.translationY = -delta
|
|
240
|
-
currentCarouselOverlayView?.translationY = -delta
|
|
241
|
-
|
|
242
|
-
// Next overlays: positioned one page ahead in the scroll direction
|
|
243
|
-
if (delta >= 0) {
|
|
244
|
-
// Forward scroll (or idle): next cell is below
|
|
245
|
-
val nextY = cellHeight - delta
|
|
246
|
-
nextOverlayView?.translationY = nextY
|
|
247
|
-
nextCarouselOverlayView?.translationY = nextY
|
|
248
|
-
} else {
|
|
249
|
-
// Backward scroll: next cell is above
|
|
250
|
-
val nextY = -cellHeight - delta
|
|
251
|
-
nextOverlayView?.translationY = nextY
|
|
252
|
-
nextCarouselOverlayView?.translationY = nextY
|
|
268
|
+
Log.e(TAG, "Unexpected error suspending feed fragment", e)
|
|
269
|
+
feedFragment = null
|
|
253
270
|
}
|
|
254
271
|
}
|
|
255
272
|
|
|
256
273
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* In Fabric interop, this view may be wrapped in an intermediate
|
|
260
|
-
* ViewGroup, so `getParent()` may not be the React `<View>` container
|
|
261
|
-
* from ShortKitFeed.tsx. We walk up the ancestor chain until we find
|
|
262
|
-
* the overlays.
|
|
274
|
+
* Called by [ShortKitFeedViewManager.onDropViewInstance] when React
|
|
275
|
+
* unmounts the native view. Performs a full fragment teardown.
|
|
263
276
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
val child = ancestor.getChildAt(i)
|
|
269
|
-
if (child === this || isOwnAncestor(child)) continue
|
|
270
|
-
|
|
271
|
-
if (currentOverlayView == null) {
|
|
272
|
-
currentOverlayView = ReactFindViewUtil.findView(child, "overlay-current")
|
|
273
|
-
}
|
|
274
|
-
if (nextOverlayView == null) {
|
|
275
|
-
nextOverlayView = ReactFindViewUtil.findView(child, "overlay-next")
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (currentOverlayView != null && nextOverlayView != null) return
|
|
279
|
-
ancestor = ancestor.parent as? ViewGroup ?: return
|
|
280
|
-
}
|
|
277
|
+
fun destroy() {
|
|
278
|
+
handler.removeCallbacks(constrainRunnable)
|
|
279
|
+
handler.removeCallbacks(layoutRunnable)
|
|
280
|
+
destroyFeedFragment()
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
|
-
*
|
|
284
|
+
* Full teardown — removes the fragment from FragmentManager entirely.
|
|
285
|
+
* Called when the Fabric view is being destroyed (not just hidden).
|
|
285
286
|
*/
|
|
286
|
-
private fun
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
val child = ancestor.getChildAt(i)
|
|
291
|
-
if (child === this || isOwnAncestor(child)) continue
|
|
292
|
-
|
|
293
|
-
if (currentCarouselOverlayView == null) {
|
|
294
|
-
currentCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-current")
|
|
295
|
-
}
|
|
296
|
-
if (nextCarouselOverlayView == null) {
|
|
297
|
-
nextCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-next")
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (currentCarouselOverlayView != null && nextCarouselOverlayView != null) return
|
|
301
|
-
ancestor = ancestor.parent as? ViewGroup ?: return
|
|
287
|
+
private fun destroyFeedFragment() {
|
|
288
|
+
feedId?.let { id ->
|
|
289
|
+
ShortKitBridge.shared?.unregisterFeed(id)
|
|
290
|
+
ShortKitBridge.shared?.unregisterFeedFragment(id)
|
|
302
291
|
}
|
|
303
|
-
|
|
292
|
+
val fragment = feedFragment ?: return
|
|
293
|
+
fragment.deactivate()
|
|
294
|
+
feedFragment = null
|
|
304
295
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
296
|
+
try {
|
|
297
|
+
val activity = getReactActivity() ?: return
|
|
298
|
+
activity.supportFragmentManager
|
|
299
|
+
.beginTransaction()
|
|
300
|
+
.remove(fragment)
|
|
301
|
+
.commitNowAllowingStateLoss()
|
|
302
|
+
} catch (e: IllegalStateException) {
|
|
303
|
+
// Expected during Activity teardown
|
|
304
|
+
} catch (e: Exception) {
|
|
305
|
+
Log.e(TAG, "Unexpected error removing feed fragment", e)
|
|
311
306
|
}
|
|
312
|
-
return false
|
|
313
307
|
}
|
|
314
308
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
309
|
+
// -----------------------------------------------------------------------
|
|
310
|
+
// Helpers
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/** Recursively find the first ViewPager2 in a view hierarchy. */
|
|
314
|
+
private fun findViewPager2(view: View): androidx.viewpager2.widget.ViewPager2? {
|
|
315
|
+
if (view is androidx.viewpager2.widget.ViewPager2) return view
|
|
318
316
|
if (view is ViewGroup) {
|
|
319
317
|
for (i in 0 until view.childCount) {
|
|
320
318
|
findViewPager2(view.getChildAt(i))?.let { return it }
|
|
@@ -323,13 +321,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
323
321
|
return null
|
|
324
322
|
}
|
|
325
323
|
|
|
326
|
-
// -----------------------------------------------------------------------
|
|
327
|
-
// Helpers
|
|
328
|
-
// -----------------------------------------------------------------------
|
|
329
|
-
|
|
330
324
|
/**
|
|
331
325
|
* Walk the context wrapper chain to find the hosting [FragmentActivity].
|
|
332
|
-
* React Native's [ThemedReactContext] wraps the Activity, so we unwrap it.
|
|
333
326
|
*/
|
|
334
327
|
private fun getReactActivity(): FragmentActivity? {
|
|
335
328
|
var ctx = context
|
|
@@ -15,16 +15,30 @@ class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
|
|
|
15
15
|
return ShortKitFeedView(context)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
@ReactProp(name = "feedId")
|
|
19
|
+
fun setFeedId(view: ShortKitFeedView, feedId: String?) {
|
|
20
|
+
view.feedId = feedId
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
@ReactProp(name = "config")
|
|
19
24
|
fun setConfig(view: ShortKitFeedView, config: String?) {
|
|
20
25
|
view.config = config
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
@ReactProp(name = "
|
|
24
|
-
fun
|
|
25
|
-
view.
|
|
28
|
+
@ReactProp(name = "startAtItemId")
|
|
29
|
+
fun setStartAtItemId(view: ShortKitFeedView, startAtItemId: String?) {
|
|
30
|
+
view.startAtItemId = startAtItemId
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
@ReactProp(name = "preloadId")
|
|
34
|
+
fun setPreloadId(view: ShortKitFeedView, preloadId: String?) {
|
|
35
|
+
view.preloadId = preloadId
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun onDropViewInstance(view: ShortKitFeedView) {
|
|
39
|
+
view.destroy()
|
|
40
|
+
super.onDropViewInstance(view)
|
|
41
|
+
}
|
|
28
42
|
|
|
29
43
|
companion object {
|
|
30
44
|
const val REACT_CLASS = "ShortKitFeedView"
|