@shortkitsdk/react-native 0.1.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/ShortKitReactNative.podspec +19 -0
- package/android/build.gradle.kts +34 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
- package/app.plugin.js +1 -0
- package/ios/ShortKitBridge.swift +537 -0
- package/ios/ShortKitFeedView.swift +207 -0
- package/ios/ShortKitFeedViewManager.mm +29 -0
- package/ios/ShortKitModule.h +25 -0
- package/ios/ShortKitModule.mm +204 -0
- package/ios/ShortKitOverlayBridge.swift +91 -0
- package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
- package/ios/ShortKitReactNative.podspec +19 -0
- package/package.json +50 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +13 -0
- package/plugin/build/withShortKitAndroid.d.ts +8 -0
- package/plugin/build/withShortKitAndroid.js +32 -0
- package/plugin/build/withShortKitIOS.d.ts +8 -0
- package/plugin/build/withShortKitIOS.js +29 -0
- package/react-native.config.js +8 -0
- package/src/OverlayManager.tsx +87 -0
- package/src/ShortKitContext.ts +51 -0
- package/src/ShortKitFeed.tsx +203 -0
- package/src/ShortKitProvider.tsx +526 -0
- package/src/index.ts +26 -0
- package/src/serialization.ts +95 -0
- package/src/specs/NativeShortKitModule.ts +201 -0
- package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
- package/src/types.ts +167 -0
- package/src/useShortKit.ts +20 -0
- package/src/useShortKitPlayer.ts +29 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
3
|
+
|
|
4
|
+
Pod::Spec.new do |s|
|
|
5
|
+
s.name = "ShortKitReactNative"
|
|
6
|
+
s.version = package["version"]
|
|
7
|
+
s.summary = package["description"]
|
|
8
|
+
s.homepage = "https://github.com/shortkit/shortkit"
|
|
9
|
+
s.license = package["license"]
|
|
10
|
+
s.author = "ShortKit"
|
|
11
|
+
s.platforms = { :ios => "16.0" }
|
|
12
|
+
s.source = { :git => "https://github.com/shortkit/shortkit.git", :tag => "v#{s.version}" }
|
|
13
|
+
s.source_files = "ios/*.{h,m,mm,cpp,swift}"
|
|
14
|
+
s.requires_arc = true
|
|
15
|
+
|
|
16
|
+
s.dependency "ShortKit"
|
|
17
|
+
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id("com.android.library")
|
|
3
|
+
id("org.jetbrains.kotlin.android")
|
|
4
|
+
id("com.facebook.react")
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
android {
|
|
8
|
+
namespace = "com.shortkit.reactnative"
|
|
9
|
+
compileSdk = 35
|
|
10
|
+
|
|
11
|
+
defaultConfig {
|
|
12
|
+
minSdk = 24
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
compileOptions {
|
|
16
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
17
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
kotlinOptions {
|
|
21
|
+
jvmTarget = "17"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sourceSets {
|
|
25
|
+
named("main") {
|
|
26
|
+
java.srcDirs("src/main/java")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
implementation("com.facebook.react:react-android")
|
|
33
|
+
implementation("dev.shortkit:shortkit:0.1.0")
|
|
34
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.ContextWrapper
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import androidx.fragment.app.FragmentActivity
|
|
10
|
+
import androidx.viewpager2.widget.ViewPager2
|
|
11
|
+
import com.facebook.react.uimanager.util.ReactFindViewUtil
|
|
12
|
+
import com.shortkit.ShortKitFeedFragment
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fabric native view that embeds [ShortKitFeedFragment] using Android
|
|
16
|
+
* Fragment transactions. Props are set by [ShortKitFeedViewManager].
|
|
17
|
+
*
|
|
18
|
+
* Also tracks the feed's scroll offset via [ViewPager2.OnPageChangeCallback]
|
|
19
|
+
* and applies native translation transforms to the sibling RN overlay views
|
|
20
|
+
* so they move with the active cell during swipe transitions.
|
|
21
|
+
*
|
|
22
|
+
* Android equivalent of iOS `ShortKitFeedView.swift`.
|
|
23
|
+
*/
|
|
24
|
+
class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
25
|
+
|
|
26
|
+
// -----------------------------------------------------------------------
|
|
27
|
+
// Props (set by ViewManager)
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
var config: String? = null
|
|
31
|
+
var overlayType: String? = null
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
// Fragment management
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
private var feedFragment: ShortKitFeedFragment? = null
|
|
38
|
+
private var fragmentContainerId: Int = View.generateViewId()
|
|
39
|
+
|
|
40
|
+
init {
|
|
41
|
+
id = fragmentContainerId
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// -----------------------------------------------------------------------
|
|
45
|
+
// Scroll tracking
|
|
46
|
+
// -----------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
private var viewPager: ViewPager2? = null
|
|
49
|
+
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
|
|
50
|
+
|
|
51
|
+
/** Overlay for the currently active cell (nativeID="overlay-current"). */
|
|
52
|
+
private var currentOverlayView: View? = null
|
|
53
|
+
|
|
54
|
+
/** Overlay for the upcoming cell (nativeID="overlay-next"). */
|
|
55
|
+
private var nextOverlayView: View? = null
|
|
56
|
+
|
|
57
|
+
/** The page index from which the current scroll gesture started. */
|
|
58
|
+
private var currentPage: Int = 0
|
|
59
|
+
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
// Lifecycle
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
override fun onAttachedToWindow() {
|
|
65
|
+
super.onAttachedToWindow()
|
|
66
|
+
embedFeedFragmentIfNeeded()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun onDetachedFromWindow() {
|
|
70
|
+
teardownScrollTracking()
|
|
71
|
+
removeFeedFragment()
|
|
72
|
+
super.onDetachedFromWindow()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -----------------------------------------------------------------------
|
|
76
|
+
// Fragment containment
|
|
77
|
+
// -----------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
private fun embedFeedFragmentIfNeeded() {
|
|
80
|
+
if (feedFragment != null) return
|
|
81
|
+
|
|
82
|
+
val sdk = ShortKitModule.shared?.sdk ?: run {
|
|
83
|
+
Log.w(TAG, "ShortKit SDK not initialized. Call ShortKitModule.initialize() first.")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val activity = getReactActivity() ?: run {
|
|
88
|
+
Log.w(TAG, "Could not find hosting Activity.")
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
val fragment = ShortKitFeedFragment.newInstance(sdk)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
activity.supportFragmentManager
|
|
96
|
+
.beginTransaction()
|
|
97
|
+
.replace(fragmentContainerId, fragment)
|
|
98
|
+
.commitNowAllowingStateLoss()
|
|
99
|
+
this.feedFragment = fragment // Only assign after successful commit
|
|
100
|
+
setupScrollTracking(fragment)
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
Log.e(TAG, "Failed to embed feed fragment", e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun removeFeedFragment() {
|
|
107
|
+
val fragment = feedFragment ?: return
|
|
108
|
+
feedFragment = null // Clear immediately to prevent leaks on early return
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
val activity = getReactActivity() ?: return
|
|
112
|
+
activity.supportFragmentManager
|
|
113
|
+
.beginTransaction()
|
|
114
|
+
.remove(fragment)
|
|
115
|
+
.commitNowAllowingStateLoss()
|
|
116
|
+
} catch (e: IllegalStateException) {
|
|
117
|
+
// Expected during Activity teardown — fragment already being destroyed
|
|
118
|
+
} catch (e: Exception) {
|
|
119
|
+
Log.e(TAG, "Unexpected error removing feed fragment", e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// Scroll tracking
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
private fun setupScrollTracking(fragment: ShortKitFeedFragment) {
|
|
128
|
+
val pager = findViewPager2(fragment.view ?: return) ?: run {
|
|
129
|
+
Log.w(TAG, "Could not find ViewPager2 in feed fragment hierarchy.")
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
viewPager = pager
|
|
133
|
+
|
|
134
|
+
val callback = object : ViewPager2.OnPageChangeCallback() {
|
|
135
|
+
override fun onPageScrolled(
|
|
136
|
+
position: Int,
|
|
137
|
+
positionOffset: Float,
|
|
138
|
+
positionOffsetPixels: Int
|
|
139
|
+
) {
|
|
140
|
+
handleScrollOffset(position, positionOffset, positionOffsetPixels)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
pageChangeCallback = callback
|
|
144
|
+
pager.registerOnPageChangeCallback(callback)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun teardownScrollTracking() {
|
|
148
|
+
pageChangeCallback?.let { viewPager?.unregisterOnPageChangeCallback(it) }
|
|
149
|
+
pageChangeCallback = null
|
|
150
|
+
viewPager = null
|
|
151
|
+
currentOverlayView?.translationY = 0f
|
|
152
|
+
nextOverlayView?.translationY = 0f
|
|
153
|
+
currentOverlayView = null
|
|
154
|
+
nextOverlayView = null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun handleScrollOffset(
|
|
158
|
+
position: Int,
|
|
159
|
+
positionOffset: Float,
|
|
160
|
+
positionOffsetPixels: Int
|
|
161
|
+
) {
|
|
162
|
+
val cellHeight = height.toFloat()
|
|
163
|
+
if (cellHeight <= 0) return
|
|
164
|
+
|
|
165
|
+
// positionOffset is 0.0 when settled, approaches 1.0 during forward scroll.
|
|
166
|
+
// position is the index of the page currently filling most of the screen.
|
|
167
|
+
val delta: Float = if (position >= currentPage) {
|
|
168
|
+
// Forward scroll (or idle)
|
|
169
|
+
positionOffsetPixels.toFloat()
|
|
170
|
+
} else {
|
|
171
|
+
// Backward scroll: position dropped below currentPage
|
|
172
|
+
-(cellHeight - positionOffsetPixels.toFloat())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update currentPage when the scroll settles
|
|
176
|
+
if (positionOffset == 0f) {
|
|
177
|
+
currentPage = position
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find the overlay views if not cached
|
|
181
|
+
if (currentOverlayView == null || nextOverlayView == null) {
|
|
182
|
+
findOverlayViews()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Current overlay follows the active cell
|
|
186
|
+
currentOverlayView?.translationY = -delta
|
|
187
|
+
|
|
188
|
+
// Next overlay: positioned one page ahead in the scroll direction
|
|
189
|
+
if (delta >= 0) {
|
|
190
|
+
// Forward scroll (or idle): next cell is below
|
|
191
|
+
nextOverlayView?.translationY = cellHeight - delta
|
|
192
|
+
} else {
|
|
193
|
+
// Backward scroll: next cell is above
|
|
194
|
+
nextOverlayView?.translationY = -cellHeight - delta
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find the sibling RN overlay views by nativeID. In the Fabric hierarchy,
|
|
200
|
+
* the ShortKitFeedView and the overlay containers are children of the
|
|
201
|
+
* same parent (wrapped by ShortKitFeed.tsx with overflow: hidden).
|
|
202
|
+
*/
|
|
203
|
+
private fun findOverlayViews() {
|
|
204
|
+
val parent = parent as? ViewGroup ?: return
|
|
205
|
+
|
|
206
|
+
for (i in 0 until parent.childCount) {
|
|
207
|
+
val sibling = parent.getChildAt(i)
|
|
208
|
+
if (sibling === this) continue
|
|
209
|
+
if (currentOverlayView == null) {
|
|
210
|
+
currentOverlayView = ReactFindViewUtil.findView(sibling, "overlay-current")
|
|
211
|
+
}
|
|
212
|
+
if (nextOverlayView == null) {
|
|
213
|
+
nextOverlayView = ReactFindViewUtil.findView(sibling, "overlay-next")
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Recursively find the first [ViewPager2] in a view hierarchy. */
|
|
219
|
+
private fun findViewPager2(view: View): ViewPager2? {
|
|
220
|
+
if (view is ViewPager2) return view
|
|
221
|
+
if (view is ViewGroup) {
|
|
222
|
+
for (i in 0 until view.childCount) {
|
|
223
|
+
findViewPager2(view.getChildAt(i))?.let { return it }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
// Helpers
|
|
231
|
+
// -----------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Walk the context wrapper chain to find the hosting [FragmentActivity].
|
|
235
|
+
* React Native's [ThemedReactContext] wraps the Activity, so we unwrap it.
|
|
236
|
+
*/
|
|
237
|
+
private fun getReactActivity(): FragmentActivity? {
|
|
238
|
+
var ctx = context
|
|
239
|
+
while (ctx is ContextWrapper) {
|
|
240
|
+
if (ctx is FragmentActivity) return ctx
|
|
241
|
+
ctx = ctx.baseContext
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
companion object {
|
|
247
|
+
private const val TAG = "ShortKitFeedView"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
|
|
9
|
+
@ReactModule(name = ShortKitFeedViewManager.REACT_CLASS)
|
|
10
|
+
class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
|
|
11
|
+
|
|
12
|
+
override fun getName(): String = REACT_CLASS
|
|
13
|
+
|
|
14
|
+
override fun createViewInstance(context: ThemedReactContext): ShortKitFeedView {
|
|
15
|
+
return ShortKitFeedView(context)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@ReactProp(name = "config")
|
|
19
|
+
fun setConfig(view: ShortKitFeedView, config: String?) {
|
|
20
|
+
view.config = config
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@ReactProp(name = "overlayType", defaultString = "none")
|
|
24
|
+
fun setOverlayType(view: ShortKitFeedView, overlayType: String?) {
|
|
25
|
+
view.overlayType = overlayType
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
companion object {
|
|
30
|
+
const val REACT_CLASS = "ShortKitFeedView"
|
|
31
|
+
}
|
|
32
|
+
}
|