@shortkitsdk/react-native 0.2.11 → 0.2.14
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/build.gradle.kts +13 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +157 -54
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +67 -56
- package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +431 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +154 -26
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +160 -35
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +5 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +45 -10
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +9 -0
- package/ios/ReactCarouselOverlayHost.swift +37 -17
- package/ios/ReactOverlayHost.swift +33 -35
- package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
- package/ios/ShortKitBridge.swift +78 -2
- package/ios/ShortKitFeedView.swift +24 -3
- package/ios/ShortKitModule.mm +6 -2
- package/ios/ShortKitSDK.xcframework/Info.plist +4 -4
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +69 -5
- 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 +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2597 -389
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +69 -5
- 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 +69 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/ShortKitSDK.xcframework.bak2/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +31351 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +865 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak2/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
- package/src/ShortKitContext.ts +2 -1
- package/src/ShortKitFeed.tsx +19 -1
- package/src/ShortKitOverlaySurface.tsx +148 -41
- package/src/ShortKitPlayer.tsx +25 -3
- package/src/ShortKitProvider.tsx +4 -2
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
- package/src/index.ts +8 -1
- package/src/serialization.ts +8 -0
- package/src/specs/NativeShortKitModule.ts +31 -1
- package/src/types.ts +45 -1
package/android/build.gradle.kts
CHANGED
|
@@ -31,8 +31,20 @@ android {
|
|
|
31
31
|
|
|
32
32
|
dependencies {
|
|
33
33
|
implementation("com.facebook.react:react-android")
|
|
34
|
-
|
|
34
|
+
// When Reco's settings.gradle has the composite build, Gradle substitutes
|
|
35
|
+
// this with the live source from android_sdk/shortkit automatically.
|
|
36
|
+
// implementation("dev.shortkit:shortkit:0.2.11")
|
|
37
|
+
implementation(files("/Users/michaelseleman/shortkit/android_sdk/shortkit/build/outputs/aar/shortkit-release.aar"))
|
|
38
|
+
// Transitive deps needed when using local AAR (Maven artifact bundles these automatically)
|
|
39
|
+
implementation("androidx.media3:media3-exoplayer:1.5.1")
|
|
40
|
+
implementation("androidx.media3:media3-exoplayer-hls:1.5.1")
|
|
41
|
+
implementation("androidx.media3:media3-datasource:1.5.1")
|
|
42
|
+
implementation("androidx.media3:media3-ui:1.5.1")
|
|
43
|
+
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
|
35
44
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
|
45
|
+
implementation("androidx.fragment:fragment-ktx:1.8.5")
|
|
46
|
+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
|
47
|
+
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
36
48
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
|
37
49
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
|
38
50
|
}
|
|
@@ -11,6 +11,12 @@ import com.facebook.react.runtime.ReactSurfaceImpl
|
|
|
11
11
|
import com.shortkit.sdk.model.CarouselImage
|
|
12
12
|
import com.shortkit.sdk.model.ImageCarouselItem
|
|
13
13
|
import com.shortkit.sdk.overlay.CarouselOverlay
|
|
14
|
+
import kotlinx.coroutines.CoroutineScope
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.Job
|
|
17
|
+
import kotlinx.coroutines.SupervisorJob
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlinx.coroutines.withContext
|
|
14
20
|
import kotlinx.serialization.encodeToString
|
|
15
21
|
import kotlinx.serialization.json.Json
|
|
16
22
|
import java.io.File
|
|
@@ -45,6 +51,17 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
45
51
|
|
|
46
52
|
private var surface: ReactSurface? = null
|
|
47
53
|
private var pendingProps: Bundle? = null
|
|
54
|
+
private var isInLayoutPass: Boolean = false
|
|
55
|
+
private val ioScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
56
|
+
private var pendingWriteJob: Job? = null
|
|
57
|
+
private var configureGeneration: Int = 0
|
|
58
|
+
|
|
59
|
+
/** Unique identifier for this overlay instance, used for event routing. */
|
|
60
|
+
val surfaceId: String = java.util.UUID.randomUUID().toString()
|
|
61
|
+
|
|
62
|
+
private var cachedItemJSON: String? = null
|
|
63
|
+
private var isActive: Boolean = false
|
|
64
|
+
private var activeImageIndex: Int = 0
|
|
48
65
|
|
|
49
66
|
// ------------------------------------------------------------------
|
|
50
67
|
// Fabric layout workaround
|
|
@@ -52,15 +69,19 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
52
69
|
|
|
53
70
|
private val layoutHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
54
71
|
|
|
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
|
+
}
|
|
79
|
+
|
|
55
80
|
override fun requestLayout() {
|
|
56
81
|
super.requestLayout()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
61
|
-
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
62
|
-
)
|
|
63
|
-
layout(left, top, right, bottom)
|
|
82
|
+
if (!isInLayoutPass) {
|
|
83
|
+
@Suppress("UNNECESSARY_SAFE_CALL")
|
|
84
|
+
layoutHandler?.post(layoutRunnable)
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
@@ -109,65 +130,140 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
109
130
|
// ------------------------------------------------------------------
|
|
110
131
|
|
|
111
132
|
override fun configure(item: ImageCarouselItem) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
isActive = false
|
|
134
|
+
activeImageIndex = 0
|
|
135
|
+
|
|
136
|
+
// Increment generation — any in-flight coroutine from a previous
|
|
137
|
+
// configure() call will see a stale generation and bail out.
|
|
138
|
+
val gen = ++configureGeneration
|
|
139
|
+
|
|
140
|
+
// Cancel any in-flight write job from a previous configure() call.
|
|
141
|
+
pendingWriteJob?.cancel()
|
|
142
|
+
|
|
143
|
+
// Fast path: check for pre-existing temp files synchronously (just
|
|
144
|
+
// file.exists(), no I/O). This avoids the flicker of remote→local
|
|
145
|
+
// URL swap for images that were cached in a previous session.
|
|
146
|
+
val lookup = cachedImage
|
|
147
|
+
val fastItem = if (lookup != null) {
|
|
148
|
+
val fastImages = item.images.map { image ->
|
|
149
|
+
val hash = image.url.hashCode()
|
|
150
|
+
val fileName = "sk-carousel-${kotlin.math.abs(hash)}.jpg"
|
|
151
|
+
val file = File(context.cacheDir, fileName)
|
|
152
|
+
if (file.exists()) CarouselImage(url = "file://${file.absolutePath}", alt = image.alt)
|
|
153
|
+
else image
|
|
126
154
|
}
|
|
127
155
|
ImageCarouselItem(
|
|
128
156
|
id = item.id,
|
|
129
|
-
images =
|
|
157
|
+
images = fastImages,
|
|
130
158
|
caption = item.caption,
|
|
131
159
|
title = item.title,
|
|
132
160
|
description = item.description,
|
|
133
161
|
author = item.author,
|
|
134
162
|
section = item.section,
|
|
135
|
-
articleUrl = item.articleUrl
|
|
163
|
+
articleUrl = item.articleUrl,
|
|
136
164
|
)
|
|
137
|
-
}
|
|
165
|
+
} else item
|
|
166
|
+
|
|
167
|
+
val json = Json.encodeToString(fastItem)
|
|
168
|
+
cachedItemJSON = json
|
|
169
|
+
pushProps()
|
|
170
|
+
|
|
171
|
+
// Pre-size the surface view NOW — before the overlay is attached to a cell.
|
|
172
|
+
val parentView = parent as? android.view.View
|
|
173
|
+
val w = if (width > 0) width
|
|
174
|
+
else if (parentView != null && parentView.width > 0) parentView.width
|
|
175
|
+
else context.resources.displayMetrics.widthPixels
|
|
176
|
+
val h = if (height > 0) height
|
|
177
|
+
else if (parentView != null && parentView.height > 0) parentView.height
|
|
178
|
+
else context.resources.displayMetrics.heightPixels
|
|
179
|
+
measureAndLayoutSurfaceView(w, h)
|
|
180
|
+
|
|
181
|
+
// Background: write any newly-cached images to temp files.
|
|
182
|
+
// Only runs if there are remote URLs remaining (not all were fast-pathed).
|
|
183
|
+
if (lookup == null) return
|
|
184
|
+
val hasRemoteUrls = fastItem.images.any { !it.url.startsWith("file://") }
|
|
185
|
+
if (!hasRemoteUrls) return
|
|
186
|
+
|
|
187
|
+
pendingWriteJob = ioScope.launch {
|
|
188
|
+
val modifiedItem = withContext(Dispatchers.IO) {
|
|
189
|
+
val localImages = item.images.map { image ->
|
|
190
|
+
val bitmap = lookup(image.url)
|
|
191
|
+
if (bitmap != null) {
|
|
192
|
+
val localUrl = writeTempImage(bitmap, image.url)
|
|
193
|
+
if (localUrl != null) CarouselImage(url = localUrl, alt = image.alt)
|
|
194
|
+
else image
|
|
195
|
+
} else {
|
|
196
|
+
image
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
ImageCarouselItem(
|
|
200
|
+
id = item.id,
|
|
201
|
+
images = localImages,
|
|
202
|
+
caption = item.caption,
|
|
203
|
+
title = item.title,
|
|
204
|
+
description = item.description,
|
|
205
|
+
author = item.author,
|
|
206
|
+
section = item.section,
|
|
207
|
+
articleUrl = item.articleUrl,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
138
210
|
|
|
139
|
-
|
|
211
|
+
// Back on Main — only update if this configure() is still current
|
|
212
|
+
if (gen != configureGeneration) return@launch
|
|
213
|
+
val hasLocalImages = modifiedItem.images.any { it.url.startsWith("file://") }
|
|
214
|
+
if (hasLocalImages) {
|
|
215
|
+
val localJson = Json.encodeToString(modifiedItem)
|
|
216
|
+
cachedItemJSON = localJson
|
|
217
|
+
pushProps()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override fun activatePlayback() {
|
|
223
|
+
isActive = true
|
|
224
|
+
val params = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
225
|
+
putString("surfaceId", surfaceId)
|
|
226
|
+
putBoolean("isActive", true)
|
|
227
|
+
putString("playerState", "idle")
|
|
228
|
+
putBoolean("isMuted", true)
|
|
229
|
+
putDouble("playbackRate", 1.0)
|
|
230
|
+
putBoolean("captionsEnabled", false)
|
|
231
|
+
putNull("activeCue")
|
|
232
|
+
putNull("feedScrollPhase")
|
|
233
|
+
}
|
|
234
|
+
ShortKitBridge.shared?.emitEvent("onOverlayFullState", params)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
override fun updateActiveImage(index: Int) {
|
|
238
|
+
activeImageIndex = index
|
|
239
|
+
val params = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
240
|
+
putString("surfaceId", surfaceId)
|
|
241
|
+
putInt("activeImageIndex", index)
|
|
242
|
+
}
|
|
243
|
+
ShortKitBridge.shared?.emitEvent("onCarouselActiveImageChanged", params)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Build a props bundle with item data and surfaceId. */
|
|
247
|
+
private fun pushProps() {
|
|
140
248
|
val bundle = Bundle().apply {
|
|
141
|
-
putString("
|
|
249
|
+
putString("surfaceId", surfaceId)
|
|
250
|
+
cachedItemJSON?.let { putString("item", it) }
|
|
142
251
|
}
|
|
252
|
+
applySurfaceProps(bundle)
|
|
253
|
+
}
|
|
143
254
|
|
|
255
|
+
/** Apply props to the surface, handling all lifecycle states. */
|
|
256
|
+
private fun applySurfaceProps(bundle: Bundle) {
|
|
144
257
|
val s = surface
|
|
145
258
|
if (s != null && s.isRunning) {
|
|
146
|
-
// Surface already running (cell reuse) — update props in place
|
|
147
259
|
(s as? ReactSurfaceImpl)?.updateInitProps(bundle)
|
|
148
260
|
} else if (s != null && !s.isRunning) {
|
|
149
|
-
// Surface pre-created by prepareSurface() but not started yet.
|
|
150
|
-
// Set props THEN start — the JS component mounts once with correct data.
|
|
151
261
|
(s as? ReactSurfaceImpl)?.updateInitProps(bundle)
|
|
152
262
|
s.start()
|
|
153
263
|
} else {
|
|
154
|
-
// No surface at all — create with item as initial props
|
|
155
264
|
pendingProps = bundle
|
|
156
265
|
createSurfaceIfNeeded()
|
|
157
266
|
}
|
|
158
|
-
|
|
159
|
-
// Pre-size the surface view NOW — before the overlay is attached to a cell.
|
|
160
|
-
// This eliminates the black flash: when the cell displays the overlay, the
|
|
161
|
-
// surface view already has correct dimensions and rendered content.
|
|
162
|
-
// Use parent dimensions if attached, otherwise use display dimensions.
|
|
163
|
-
val parentView = parent as? android.view.View
|
|
164
|
-
val w = if (width > 0) width
|
|
165
|
-
else if (parentView != null && parentView.width > 0) parentView.width
|
|
166
|
-
else context.resources.displayMetrics.widthPixels
|
|
167
|
-
val h = if (height > 0) height
|
|
168
|
-
else if (parentView != null && parentView.height > 0) parentView.height
|
|
169
|
-
else context.resources.displayMetrics.heightPixels
|
|
170
|
-
measureAndLayoutSurfaceView(w, h)
|
|
171
267
|
}
|
|
172
268
|
|
|
173
269
|
/**
|
|
@@ -225,18 +321,23 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
225
321
|
}
|
|
226
322
|
|
|
227
323
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
324
|
+
isInLayoutPass = true
|
|
325
|
+
try {
|
|
326
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
327
|
+
val w = right - left
|
|
328
|
+
val h = bottom - top
|
|
329
|
+
if (w > 0 && h > 0) {
|
|
330
|
+
measureAndLayoutSurfaceView(w, h)
|
|
331
|
+
} else {
|
|
332
|
+
val parentView = parent as? android.view.View
|
|
333
|
+
val pw = parentView?.width ?: 0
|
|
334
|
+
val ph = parentView?.height ?: 0
|
|
335
|
+
if (pw > 0 && ph > 0) {
|
|
336
|
+
measureAndLayoutSurfaceView(pw, ph)
|
|
337
|
+
}
|
|
239
338
|
}
|
|
339
|
+
} finally {
|
|
340
|
+
isInLayoutPass = false
|
|
240
341
|
}
|
|
241
342
|
}
|
|
242
343
|
|
|
@@ -312,6 +413,8 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
312
413
|
|
|
313
414
|
override fun onDetachedFromWindow() {
|
|
314
415
|
super.onDetachedFromWindow()
|
|
416
|
+
pendingWriteJob?.cancel()
|
|
417
|
+
pendingWriteJob = null
|
|
315
418
|
if (surface?.isRunning == true) {
|
|
316
419
|
surface?.stop()
|
|
317
420
|
}
|
|
@@ -60,19 +60,24 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
60
60
|
|
|
61
61
|
private val layoutHandler = Handler(Looper.getMainLooper())
|
|
62
62
|
|
|
63
|
+
/** Reusable runnable to avoid lambda allocation on every requestLayout. */
|
|
64
|
+
private val layoutRunnable = Runnable {
|
|
65
|
+
measure(
|
|
66
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
67
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
68
|
+
)
|
|
69
|
+
layout(left, top, right, bottom)
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* Fabric may suppress layout propagation to native child views inside
|
|
65
74
|
* the SDK's RecyclerView cells. Override to force a manual layout pass.
|
|
66
75
|
*/
|
|
67
76
|
override fun requestLayout() {
|
|
68
77
|
super.requestLayout()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
73
|
-
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
74
|
-
)
|
|
75
|
-
layout(left, top, right, bottom)
|
|
78
|
+
if (!isInLayoutPass) {
|
|
79
|
+
@Suppress("UNNECESSARY_SAFE_CALL")
|
|
80
|
+
layoutHandler?.post(layoutRunnable)
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -103,6 +108,7 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
103
108
|
private var timeCoalesceRunnable: Runnable? = null
|
|
104
109
|
|
|
105
110
|
private var flowScope: CoroutineScope? = null
|
|
111
|
+
private var isInLayoutPass: Boolean = false
|
|
106
112
|
|
|
107
113
|
// ------------------------------------------------------------------
|
|
108
114
|
// Init
|
|
@@ -147,14 +153,17 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
147
153
|
resubscribeToPlayer(player)
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
/** Whether the surface has been started at least once. */
|
|
157
|
+
private var surfaceHasStarted: Boolean = false
|
|
158
|
+
|
|
150
159
|
override fun configure(item: ContentItem) {
|
|
160
|
+
val isSameItem = item.id == currentItem?.id
|
|
151
161
|
currentItem = item
|
|
152
162
|
isActive = false
|
|
153
163
|
timeDirty = false
|
|
154
164
|
stopTimeCoalescing()
|
|
155
165
|
|
|
156
166
|
// Reset ALL cached state so recycled cells don't flash stale values.
|
|
157
|
-
// Mirrors iOS ReactOverlayHost.configure(with:).
|
|
158
167
|
cachedCurrentTime = 0.0
|
|
159
168
|
cachedDuration = 0.0
|
|
160
169
|
cachedBuffered = 0.0
|
|
@@ -164,12 +173,25 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
164
173
|
|
|
165
174
|
if (surface == null) {
|
|
166
175
|
createSurfaceIfNeeded()
|
|
167
|
-
|
|
176
|
+
surfaceHasStarted = true
|
|
177
|
+
} else if (!surfaceHasStarted) {
|
|
168
178
|
(surface as? ReactSurfaceImpl)?.updateInitProps(buildInitialPropsBundle())
|
|
179
|
+
surfaceHasStarted = true
|
|
180
|
+
} else if (isSameItem) {
|
|
181
|
+
// Same item = deactivation only. The overlay already has this
|
|
182
|
+
// item's data from a previous swipe. Just deactivate (isActive
|
|
183
|
+
// set to false above, timer stopped). No event emission — avoids
|
|
184
|
+
// broadcasting to all 7 surfaces for no visual change.
|
|
185
|
+
} else {
|
|
186
|
+
// Different item — send via event for React tree diff (not
|
|
187
|
+
// updateInitProps which causes full Fabric remount).
|
|
188
|
+
val params = Arguments.createMap().apply {
|
|
189
|
+
putString("surfaceId", surfaceId)
|
|
190
|
+
putString("item", ShortKitBridge.serializeContentItemToJSON(item))
|
|
191
|
+
}
|
|
192
|
+
ShortKitBridge.shared?.emitEvent("onOverlayItemChanged", params)
|
|
169
193
|
}
|
|
170
|
-
|
|
171
|
-
// Pre-size the surface view NOW — before the overlay is attached to a cell.
|
|
172
|
-
// Eliminates the black flash on cell display.
|
|
194
|
+
// Pre-size the surface view
|
|
173
195
|
val parentView = parent as? android.view.View
|
|
174
196
|
val w = if (width > 0) width
|
|
175
197
|
else if (parentView != null && parentView.width > 0) parentView.width
|
|
@@ -184,9 +206,7 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
184
206
|
isActive = true
|
|
185
207
|
startTimeCoalescing()
|
|
186
208
|
|
|
187
|
-
// Defer the event burst to the next tick.
|
|
188
|
-
// mount and establish event subscriptions (useEffect runs after render).
|
|
189
|
-
// Mirrors iOS: DispatchQueue.main.async { self.emitFullState() }
|
|
209
|
+
// Defer the event burst to the next tick.
|
|
190
210
|
handler.post {
|
|
191
211
|
if (isActive) {
|
|
192
212
|
emitFullState()
|
|
@@ -195,13 +215,16 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
195
215
|
}
|
|
196
216
|
|
|
197
217
|
override fun fadeOutForTransition() {
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
// No-op. The JS overlay component handles fade via feedScrollPhase
|
|
219
|
+
// prop (dragging/settled), not these events. iOS ReactOverlayHost
|
|
220
|
+
// doesn't implement these methods at all. The previous implementation
|
|
221
|
+
// serialized the full ContentItem to JSON and emitted across the
|
|
222
|
+
// bridge to a listener that doesn't exist — 2-15ms of pure waste
|
|
223
|
+
// on every swipe.
|
|
200
224
|
}
|
|
201
225
|
|
|
202
226
|
override fun restoreFromTransition() {
|
|
203
|
-
|
|
204
|
-
ShortKitBridge.shared?.emitOverlayEvent("onOverlayRestore", item)
|
|
227
|
+
// No-op. See fadeOutForTransition comment.
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
// ------------------------------------------------------------------
|
|
@@ -257,19 +280,24 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
257
280
|
* calling setMinimumSize/setMaximumSize in ReactOverlayHost.swift.
|
|
258
281
|
*/
|
|
259
282
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
isInLayoutPass = true
|
|
284
|
+
try {
|
|
285
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
286
|
+
val w = right - left
|
|
287
|
+
val h = bottom - top
|
|
288
|
+
if (w > 0 && h > 0) {
|
|
289
|
+
measureAndLayoutSurfaceView(w, h)
|
|
290
|
+
} else {
|
|
291
|
+
// Host still 0x0 — try parent dimensions
|
|
292
|
+
val parentView = parent as? android.view.View
|
|
293
|
+
val pw = parentView?.width ?: 0
|
|
294
|
+
val ph = parentView?.height ?: 0
|
|
295
|
+
if (pw > 0 && ph > 0) {
|
|
296
|
+
measureAndLayoutSurfaceView(pw, ph)
|
|
297
|
+
}
|
|
272
298
|
}
|
|
299
|
+
} finally {
|
|
300
|
+
isInLayoutPass = false
|
|
273
301
|
}
|
|
274
302
|
}
|
|
275
303
|
|
|
@@ -289,7 +317,9 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
289
317
|
private fun createSurfaceIfNeeded() {
|
|
290
318
|
if (surface != null) return
|
|
291
319
|
|
|
292
|
-
val
|
|
320
|
+
val appContext = context.applicationContext
|
|
321
|
+
val reactHost = (appContext as? ReactApplication)?.reactHost
|
|
322
|
+
|
|
293
323
|
if (reactHost == null) {
|
|
294
324
|
android.util.Log.e(TAG, "[$surfaceId] createSurface FAILED: reactHost is null")
|
|
295
325
|
return
|
|
@@ -310,10 +340,10 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
310
340
|
} else {
|
|
311
341
|
android.util.Log.e(TAG, "[$surfaceId] createSurface: surfaceView is NULL")
|
|
312
342
|
}
|
|
343
|
+
|
|
313
344
|
newSurface.start()
|
|
314
345
|
|
|
315
|
-
// The host may still be 0x0 at this point
|
|
316
|
-
// out the cell yet). Use the parent's dimensions if available.
|
|
346
|
+
// The host may still be 0x0 at this point. Use the parent's dimensions.
|
|
317
347
|
val parentView = parent as? android.view.View
|
|
318
348
|
val w = if (width > 0) width else parentView?.width ?: 0
|
|
319
349
|
val h = if (height > 0) height else parentView?.height ?: 0
|
|
@@ -519,41 +549,22 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
519
549
|
private fun emitFullState() {
|
|
520
550
|
val bridge = ShortKitBridge.shared ?: return
|
|
521
551
|
|
|
522
|
-
|
|
552
|
+
val params = Arguments.createMap().apply {
|
|
523
553
|
putString("surfaceId", surfaceId)
|
|
524
554
|
putBoolean("isActive", true)
|
|
525
|
-
})
|
|
526
|
-
bridge.emitEvent("onOverlayPlayerStateChanged", Arguments.createMap().apply {
|
|
527
|
-
putString("surfaceId", surfaceId)
|
|
528
555
|
putString("playerState", cachedPlayerState)
|
|
529
|
-
})
|
|
530
|
-
bridge.emitEvent("onOverlayMutedChanged", Arguments.createMap().apply {
|
|
531
|
-
putString("surfaceId", surfaceId)
|
|
532
556
|
putBoolean("isMuted", cachedIsMuted)
|
|
533
|
-
})
|
|
534
|
-
bridge.emitEvent("onOverlayPlaybackRateChanged", Arguments.createMap().apply {
|
|
535
|
-
putString("surfaceId", surfaceId)
|
|
536
557
|
putDouble("playbackRate", cachedPlaybackRate)
|
|
537
|
-
})
|
|
538
|
-
bridge.emitEvent("onOverlayCaptionsEnabledChanged", Arguments.createMap().apply {
|
|
539
|
-
putString("surfaceId", surfaceId)
|
|
540
558
|
putBoolean("captionsEnabled", cachedCaptionsEnabled)
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
bridge.emitEvent("onOverlayActiveCueChanged", Arguments.createMap().apply {
|
|
544
|
-
putString("surfaceId", surfaceId)
|
|
545
559
|
val cueJson = cachedActiveCue?.toString()
|
|
546
560
|
if (cueJson != null) {
|
|
547
561
|
putString("activeCue", cueJson)
|
|
548
562
|
} else {
|
|
549
563
|
putNull("activeCue")
|
|
550
564
|
}
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
bridge.emitEvent("onOverlayFeedScrollPhaseChanged", Arguments.createMap().apply {
|
|
554
|
-
putString("surfaceId", surfaceId)
|
|
555
565
|
cachedFeedScrollPhase?.let { putString("feedScrollPhase", it) }
|
|
556
566
|
?: putNull("feedScrollPhase")
|
|
557
|
-
}
|
|
567
|
+
}
|
|
568
|
+
bridge.emitEvent("onOverlayFullState", params)
|
|
558
569
|
}
|
|
559
570
|
}
|