stream-chat-react-native 8.13.6 → 9.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +55 -1
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +8 -1
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerFrameLayout.kt +260 -0
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerViewManager.kt +85 -0
- package/ios/shared/StreamShimmerView.swift +249 -0
- package/ios/shared/StreamShimmerViewComponentView.h +22 -0
- package/ios/shared/StreamShimmerViewComponentView.mm +108 -0
- package/package.json +16 -7
- package/src/index.js +2 -0
- package/src/native/StreamShimmerViewNativeComponent.ts +15 -0
- package/src/optionalDependencies/NativeShimmerView.ts +3 -0
- package/src/optionalDependencies/Video.tsx +14 -1
- package/src/optionalDependencies/index.ts +1 -0
- package/stream-chat-react-native.podspec +10 -27
package/android/build.gradle
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
def kotlinVersion =
|
|
2
|
+
rootProject.ext.has("kotlinVersion")
|
|
3
|
+
? rootProject.ext.get("kotlinVersion")
|
|
4
|
+
: project.properties["ImageResizer_kotlinVersion"]
|
|
5
|
+
|
|
1
6
|
buildscript {
|
|
2
7
|
repositories {
|
|
3
8
|
google()
|
|
@@ -6,7 +11,7 @@ buildscript {
|
|
|
6
11
|
|
|
7
12
|
dependencies {
|
|
8
13
|
classpath "com.android.tools.build:gradle:7.2.1"
|
|
9
|
-
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
|
10
15
|
}
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -15,6 +20,7 @@ def isNewArchitectureEnabled() {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
apply plugin: "com.android.library"
|
|
23
|
+
apply plugin: "kotlin-android"
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
|
|
@@ -30,6 +36,11 @@ def getExtOrDefault(name) {
|
|
|
30
36
|
def getExtOrIntegerDefault(name) {
|
|
31
37
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
|
|
32
38
|
}
|
|
39
|
+
def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
|
|
40
|
+
def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
|
|
41
|
+
def hasNativeSources = { File dir ->
|
|
42
|
+
dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
android {
|
|
35
46
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
@@ -65,11 +76,16 @@ android {
|
|
|
65
76
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
kotlinOptions {
|
|
80
|
+
jvmTarget = "17"
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
sourceSets {
|
|
69
84
|
main {
|
|
70
85
|
if (isNewArchitectureEnabled()) {
|
|
71
86
|
java.srcDirs += [
|
|
72
87
|
"src/newarch",
|
|
88
|
+
"src/main/java/com/streamchatreactnative/shared",
|
|
73
89
|
// This is needed to build Kotlin project with NewArch enabled
|
|
74
90
|
"${project.buildDir}/generated/source/codegen/java"
|
|
75
91
|
]
|
|
@@ -80,6 +96,44 @@ android {
|
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
tasks.register("syncSharedShimmerSources") {
|
|
100
|
+
outputs.dir(localSharedNativeRootDir)
|
|
101
|
+
outputs.upToDateWhen { false }
|
|
102
|
+
doLast {
|
|
103
|
+
def sourceRootDir = null
|
|
104
|
+
if (hasNativeSources(localSharedNativeRootDir)) {
|
|
105
|
+
sourceRootDir = localSharedNativeRootDir
|
|
106
|
+
} else if (hasNativeSources(sharedNativeRootDir)) {
|
|
107
|
+
sourceRootDir = sharedNativeRootDir
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (sourceRootDir == null) {
|
|
111
|
+
throw new GradleException(
|
|
112
|
+
"Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " +
|
|
113
|
+
"or ../../shared-native/android/**/*.{kt,java}."
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (sourceRootDir != localSharedNativeRootDir) {
|
|
118
|
+
project.delete(localSharedNativeRootDir)
|
|
119
|
+
project.copy {
|
|
120
|
+
from(sourceRootDir)
|
|
121
|
+
into(localSharedNativeRootDir)
|
|
122
|
+
}
|
|
123
|
+
} else if (!hasNativeSources(localSharedNativeRootDir)) {
|
|
124
|
+
throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
tasks.matching { it.name == "preBuild" }.configureEach {
|
|
130
|
+
dependsOn("syncSharedShimmerSources")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
|
134
|
+
dependsOn("syncSharedShimmerSources")
|
|
135
|
+
}
|
|
136
|
+
|
|
83
137
|
repositories {
|
|
84
138
|
mavenCentral()
|
|
85
139
|
google()
|
|
@@ -6,9 +6,11 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
6
6
|
import com.facebook.react.module.model.ReactModuleInfo;
|
|
7
7
|
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
8
8
|
import com.facebook.react.TurboReactPackage;
|
|
9
|
-
|
|
9
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
10
10
|
|
|
11
11
|
import java.util.HashMap;
|
|
12
|
+
import java.util.Collections;
|
|
13
|
+
import java.util.List;
|
|
12
14
|
import java.util.Map;
|
|
13
15
|
|
|
14
16
|
public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
@@ -42,4 +44,9 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
|
42
44
|
return moduleInfos;
|
|
43
45
|
};
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
50
|
+
return Collections.<ViewManager>singletonList(new StreamShimmerViewManager());
|
|
51
|
+
}
|
|
45
52
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
package com.streamchatreactnative
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.LinearGradient
|
|
8
|
+
import android.graphics.Matrix
|
|
9
|
+
import android.graphics.Paint
|
|
10
|
+
import android.graphics.Shader
|
|
11
|
+
import android.util.AttributeSet
|
|
12
|
+
import android.view.View
|
|
13
|
+
import android.view.animation.LinearInterpolator
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Native shimmer container used by `StreamShimmerView`.
|
|
19
|
+
*
|
|
20
|
+
* This view draws a base color plus a moving highlight strip directly on canvas and still behaves
|
|
21
|
+
* like a regular container for React children. The animation runs fully on the native side so it
|
|
22
|
+
* does not depend on JS-driven frame updates. It automatically stops animating when the view is
|
|
23
|
+
* detached or not visible, rebuilds its shader when size or colors change, and waits for valid
|
|
24
|
+
* dimensions before starting animation to avoid invalid draw/animation states.
|
|
25
|
+
*/
|
|
26
|
+
class StreamShimmerFrameLayout @JvmOverloads constructor(
|
|
27
|
+
context: Context,
|
|
28
|
+
attrs: AttributeSet? = null,
|
|
29
|
+
) : FrameLayout(context, attrs) {
|
|
30
|
+
private var baseColor: Int = DEFAULT_BASE_COLOR
|
|
31
|
+
private var durationMs: Long = DEFAULT_DURATION_MS
|
|
32
|
+
private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
|
|
33
|
+
private var enabled: Boolean = true
|
|
34
|
+
|
|
35
|
+
private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
|
36
|
+
private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
37
|
+
style = Paint.Style.FILL
|
|
38
|
+
isDither = true
|
|
39
|
+
}
|
|
40
|
+
private val shimmerMatrix = Matrix()
|
|
41
|
+
|
|
42
|
+
private var shimmerShader: LinearGradient? = null
|
|
43
|
+
private var shimmerTranslateX: Float = 0f
|
|
44
|
+
private var animatedDurationMs: Long = 0L
|
|
45
|
+
private var animatedViewWidth: Float = 0f
|
|
46
|
+
private var animator: ValueAnimator? = null
|
|
47
|
+
|
|
48
|
+
init {
|
|
49
|
+
setWillNotDraw(false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun setBaseColor(color: Int) {
|
|
53
|
+
if (baseColor == color) return
|
|
54
|
+
baseColor = color
|
|
55
|
+
rebuildShimmerShader()
|
|
56
|
+
invalidate()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun setGradientColor(color: Int) {
|
|
60
|
+
if (gradientColor == color) return
|
|
61
|
+
gradientColor = color
|
|
62
|
+
rebuildShimmerShader()
|
|
63
|
+
invalidate()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun setDuration(duration: Int) {
|
|
67
|
+
val normalizedDurationMs =
|
|
68
|
+
if (duration > 0) duration.toLong() else DEFAULT_DURATION_MS
|
|
69
|
+
if (durationMs == normalizedDurationMs) return
|
|
70
|
+
durationMs = normalizedDurationMs
|
|
71
|
+
updateAnimatorState()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun setShimmerEnabled(enabled: Boolean) {
|
|
75
|
+
if (this.enabled == enabled) return
|
|
76
|
+
this.enabled = enabled
|
|
77
|
+
updateAnimatorState()
|
|
78
|
+
invalidate()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun updateAnimatorState() {
|
|
82
|
+
// Centralized lifecycle gate for animation start/stop. This keeps shimmer off for detached or
|
|
83
|
+
// hidden views to avoid wasting UI-thread work in long lists.
|
|
84
|
+
if (shouldAnimateShimmer()) {
|
|
85
|
+
startShimmer()
|
|
86
|
+
} else {
|
|
87
|
+
stopShimmer()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override fun onAttachedToWindow() {
|
|
92
|
+
super.onAttachedToWindow()
|
|
93
|
+
// Reattachment (including reparenting) should recheck visibility state and restart only if
|
|
94
|
+
// this instance is eligible to animate.
|
|
95
|
+
updateAnimatorState()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onDetachedFromWindow() {
|
|
99
|
+
// Detached views are not drawable; stop and clear animator so a future attach starts cleanly.
|
|
100
|
+
stopShimmer()
|
|
101
|
+
super.onDetachedFromWindow()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
105
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
106
|
+
rebuildShimmerShader()
|
|
107
|
+
updateAnimatorState()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun onWindowVisibilityChanged(visibility: Int) {
|
|
111
|
+
super.onWindowVisibilityChanged(visibility)
|
|
112
|
+
updateAnimatorState()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
116
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
117
|
+
if (changedView === this) {
|
|
118
|
+
updateAnimatorState()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
123
|
+
val viewWidth = width.toFloat()
|
|
124
|
+
val viewHeight = height.toFloat()
|
|
125
|
+
if (viewWidth <= 0f || viewHeight <= 0f) {
|
|
126
|
+
super.dispatchDraw(canvas)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
basePaint.color = baseColor
|
|
131
|
+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint)
|
|
132
|
+
|
|
133
|
+
drawShimmer(canvas, viewWidth, viewHeight)
|
|
134
|
+
super.dispatchDraw(canvas)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
|
|
138
|
+
if (!enabled) return
|
|
139
|
+
|
|
140
|
+
val shader = shimmerShader ?: return
|
|
141
|
+
|
|
142
|
+
shimmerMatrix.setTranslate(shimmerTranslateX, 0f)
|
|
143
|
+
shader.setLocalMatrix(shimmerMatrix)
|
|
144
|
+
shimmerPaint.shader = shader
|
|
145
|
+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint)
|
|
146
|
+
shimmerPaint.shader = null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun rebuildShimmerShader() {
|
|
150
|
+
// Recreates the shimmer gradient for the current width/colors. This allocates shader state,
|
|
151
|
+
// so keep calls tied to real changes (size or color updates), not per frame execution.
|
|
152
|
+
val viewWidth = width.toFloat()
|
|
153
|
+
if (viewWidth <= 0f) {
|
|
154
|
+
shimmerShader = null
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look.
|
|
159
|
+
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
|
|
160
|
+
val transparentHighlight = colorWithAlpha(gradientColor, 0f)
|
|
161
|
+
val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR)
|
|
162
|
+
val softBase = colorWithAlpha(gradientColor, SOFT_HIGHLIGHT_ALPHA_FACTOR)
|
|
163
|
+
val mediumBase = colorWithAlpha(gradientColor, MID_HIGHLIGHT_ALPHA_FACTOR)
|
|
164
|
+
val innerBase = colorWithAlpha(gradientColor, INNER_HIGHLIGHT_ALPHA_FACTOR)
|
|
165
|
+
shimmerShader = LinearGradient(
|
|
166
|
+
0f,
|
|
167
|
+
0f,
|
|
168
|
+
shimmerWidth,
|
|
169
|
+
0f,
|
|
170
|
+
intArrayOf(
|
|
171
|
+
transparentHighlight,
|
|
172
|
+
edgeBase,
|
|
173
|
+
softBase,
|
|
174
|
+
mediumBase,
|
|
175
|
+
innerBase,
|
|
176
|
+
gradientColor,
|
|
177
|
+
innerBase,
|
|
178
|
+
mediumBase,
|
|
179
|
+
softBase,
|
|
180
|
+
edgeBase,
|
|
181
|
+
transparentHighlight,
|
|
182
|
+
),
|
|
183
|
+
floatArrayOf(
|
|
184
|
+
0f,
|
|
185
|
+
0.08f,
|
|
186
|
+
0.2f,
|
|
187
|
+
0.32f,
|
|
188
|
+
0.4f,
|
|
189
|
+
0.5f,
|
|
190
|
+
0.6f,
|
|
191
|
+
0.68f,
|
|
192
|
+
0.8f,
|
|
193
|
+
0.92f,
|
|
194
|
+
1f,
|
|
195
|
+
),
|
|
196
|
+
Shader.TileMode.CLAMP,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun startShimmer() {
|
|
201
|
+
val viewWidth = width.toFloat()
|
|
202
|
+
if (viewWidth <= 0f) return
|
|
203
|
+
// Keep the existing animator only when size and duration still match the current request.
|
|
204
|
+
if (animator != null && animatedViewWidth == viewWidth && animatedDurationMs == durationMs) return
|
|
205
|
+
|
|
206
|
+
stopShimmer()
|
|
207
|
+
|
|
208
|
+
// Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
|
|
209
|
+
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
|
|
210
|
+
animatedViewWidth = viewWidth
|
|
211
|
+
animatedDurationMs = durationMs
|
|
212
|
+
animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
|
|
213
|
+
duration = durationMs
|
|
214
|
+
repeatCount = ValueAnimator.INFINITE
|
|
215
|
+
interpolator = LinearInterpolator()
|
|
216
|
+
addUpdateListener {
|
|
217
|
+
shimmerTranslateX = it.animatedValue as Float
|
|
218
|
+
invalidate()
|
|
219
|
+
}
|
|
220
|
+
start()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private fun stopShimmer() {
|
|
225
|
+
animator?.cancel()
|
|
226
|
+
animator = null
|
|
227
|
+
animatedDurationMs = 0L
|
|
228
|
+
animatedViewWidth = 0f
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private fun shouldAnimateShimmer(): Boolean {
|
|
232
|
+
// `isShown` and explicit visibility/window checks cover different hide paths in nested
|
|
233
|
+
// hierarchies. Keeping them all prevents animations running when not visible to the user.
|
|
234
|
+
return enabled &&
|
|
235
|
+
isAttachedToWindow &&
|
|
236
|
+
width > 0 &&
|
|
237
|
+
height > 0 &&
|
|
238
|
+
visibility == View.VISIBLE &&
|
|
239
|
+
windowVisibility == View.VISIBLE &&
|
|
240
|
+
isShown &&
|
|
241
|
+
alpha > 0f
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun colorWithAlpha(color: Int, alphaFactor: Float): Int {
|
|
245
|
+
// Preserve RGB while shaping only alpha; used for symmetric highlight falloff in gradient stops.
|
|
246
|
+
val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255)
|
|
247
|
+
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
companion object {
|
|
251
|
+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
|
|
252
|
+
private const val DEFAULT_DURATION_MS = 1200L
|
|
253
|
+
private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
|
|
254
|
+
private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
|
|
255
|
+
private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
|
|
256
|
+
private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f
|
|
257
|
+
private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.48f
|
|
258
|
+
private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.72f
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.streamchatreactnative
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull
|
|
4
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
|
+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fabric manager for StreamShimmerView.
|
|
12
|
+
*
|
|
13
|
+
* It creates the native shimmer layout, maps React props to native setters, and exposes child
|
|
14
|
+
* management methods so Fabric can mount and unmount children correctly inside this container.
|
|
15
|
+
* The manager rechecks animation state after prop transactions and disables shimmer when a view
|
|
16
|
+
* instance is dropped as a defensive cleanup step for recycled or unmounted views. Because the
|
|
17
|
+
* shimmer view wraps React children, this must remain a real ViewGroupManager as using a non-group
|
|
18
|
+
* manager can fail in Fabric mounting paths at runtime.
|
|
19
|
+
*/
|
|
20
|
+
class StreamShimmerViewManager : ViewGroupManager<StreamShimmerFrameLayout>(),
|
|
21
|
+
StreamShimmerViewManagerInterface<StreamShimmerFrameLayout> {
|
|
22
|
+
private val delegate = StreamShimmerViewManagerDelegate(this)
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = REACT_CLASS
|
|
25
|
+
|
|
26
|
+
@NonNull
|
|
27
|
+
override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout {
|
|
28
|
+
val layout = StreamShimmerFrameLayout(reactContext)
|
|
29
|
+
layout.updateAnimatorState()
|
|
30
|
+
return layout
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) {
|
|
34
|
+
super.onAfterUpdateTransaction(view)
|
|
35
|
+
// Prop batches can change visibility/enabled/colors together, so we re-evaluate the animator once
|
|
36
|
+
// after every transaction to keep state consistent and avoid duplicate start/stop churn.
|
|
37
|
+
view.updateAnimatorState()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) {
|
|
41
|
+
parent.addView(child, index)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View {
|
|
45
|
+
return parent.getChildAt(index)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun getChildCount(parent: StreamShimmerFrameLayout): Int {
|
|
49
|
+
return parent.childCount
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) {
|
|
53
|
+
parent.removeViewAt(index)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun getDelegate(): ViewManagerDelegate<StreamShimmerFrameLayout> = delegate
|
|
57
|
+
|
|
58
|
+
override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) {
|
|
59
|
+
view.setShimmerEnabled(enabled)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) {
|
|
63
|
+
view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun setDuration(view: StreamShimmerFrameLayout, duration: Int) {
|
|
67
|
+
view.setDuration(duration)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
|
|
71
|
+
view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) {
|
|
75
|
+
super.onDropViewInstance(view)
|
|
76
|
+
// Defensive shutdown for recycled/unmounted views; avoids animator leaks in list-heavy screens.
|
|
77
|
+
view.setShimmerEnabled(false)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
companion object {
|
|
81
|
+
const val REACT_CLASS = "StreamShimmerView"
|
|
82
|
+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
|
|
83
|
+
private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import QuartzCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Native shimmer view used by the Fabric component view.
|
|
5
|
+
///
|
|
6
|
+
/// It renders a base layer and a moving gradient highlight entirely in native code, so shimmer
|
|
7
|
+
/// animation stays off the JS thread. The view updates its gradient when size or colors change and
|
|
8
|
+
/// stops animation when it is not drawable (backgrounded, detached, hidden, or zero sized).
|
|
9
|
+
@objcMembers
|
|
10
|
+
public final class StreamShimmerView: UIView {
|
|
11
|
+
private static let edgeHighlightAlpha: CGFloat = 0.1
|
|
12
|
+
private static let softHighlightAlpha: CGFloat = 0.24
|
|
13
|
+
private static let midHighlightAlpha: CGFloat = 0.48
|
|
14
|
+
private static let innerHighlightAlpha: CGFloat = 0.72
|
|
15
|
+
private static let defaultHighlightAlpha: CGFloat = 0.35
|
|
16
|
+
private static let defaultShimmerDuration: CFTimeInterval = 1.2
|
|
17
|
+
private static let shimmerStripWidthRatio: CGFloat = 1.25
|
|
18
|
+
private static let shimmerAnimationKey = "stream_shimmer_translate_x"
|
|
19
|
+
|
|
20
|
+
private let baseLayer = CALayer()
|
|
21
|
+
private let shimmerLayer = CAGradientLayer()
|
|
22
|
+
|
|
23
|
+
private var baseColor: UIColor = UIColor(white: 1, alpha: 0)
|
|
24
|
+
private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha)
|
|
25
|
+
private var enabled = false
|
|
26
|
+
private var shimmerDuration: CFTimeInterval = defaultShimmerDuration
|
|
27
|
+
private var lastAnimatedDuration: CFTimeInterval = 0
|
|
28
|
+
private var lastAnimatedSize: CGSize = .zero
|
|
29
|
+
private var isAppActive = true
|
|
30
|
+
|
|
31
|
+
public override init(frame: CGRect) {
|
|
32
|
+
super.init(frame: frame)
|
|
33
|
+
setupLayers()
|
|
34
|
+
setupLifecycleObservers()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public required init?(coder: NSCoder) {
|
|
38
|
+
super.init(coder: coder)
|
|
39
|
+
setupLayers()
|
|
40
|
+
setupLifecycleObservers()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deinit {
|
|
44
|
+
NotificationCenter.default.removeObserver(self)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public override func layoutSubviews() {
|
|
48
|
+
super.layoutSubviews()
|
|
49
|
+
updateLayersForCurrentState()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public override func didMoveToWindow() {
|
|
53
|
+
super.didMoveToWindow()
|
|
54
|
+
if window == nil {
|
|
55
|
+
// Detaching from window means this view is no longer drawable. Stop and clear animation so
|
|
56
|
+
// a later reattach starts from a clean state.
|
|
57
|
+
stopAnimation()
|
|
58
|
+
} else {
|
|
59
|
+
// Reattaching (including reparenting across windows) re-evaluates state and restarts only
|
|
60
|
+
// when needed by current bounds/visibility/enablement.
|
|
61
|
+
updateLayersForCurrentState()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
66
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
67
|
+
if let previousTraitCollection,
|
|
68
|
+
traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
|
|
69
|
+
{
|
|
70
|
+
// In current usage, colors are typically driven by JS props. We still refresh on trait
|
|
71
|
+
// changes so dynamically resolved native colors remain correct if that path is used later.
|
|
72
|
+
updateLayersForCurrentState()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public func apply(
|
|
77
|
+
baseColor: UIColor,
|
|
78
|
+
gradientColor: UIColor,
|
|
79
|
+
durationMilliseconds: Double,
|
|
80
|
+
enabled: Bool
|
|
81
|
+
) {
|
|
82
|
+
self.baseColor = baseColor
|
|
83
|
+
self.gradientColor = gradientColor
|
|
84
|
+
shimmerDuration = Self.normalizedDuration(milliseconds: durationMilliseconds)
|
|
85
|
+
self.enabled = enabled
|
|
86
|
+
updateLayersForCurrentState()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func stopAnimation() {
|
|
90
|
+
shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey)
|
|
91
|
+
lastAnimatedDuration = 0
|
|
92
|
+
lastAnimatedSize = .zero
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func setupLayers() {
|
|
96
|
+
isUserInteractionEnabled = false
|
|
97
|
+
|
|
98
|
+
shimmerLayer.contentsScale = UIScreen.main.scale
|
|
99
|
+
shimmerLayer.allowsEdgeAntialiasing = true
|
|
100
|
+
shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
101
|
+
shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
102
|
+
shimmerLayer.locations = [0.0, 0.08, 0.2, 0.32, 0.4, 0.5, 0.6, 0.68, 0.8, 0.92, 1.0]
|
|
103
|
+
|
|
104
|
+
layer.addSublayer(baseLayer)
|
|
105
|
+
layer.addSublayer(shimmerLayer)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func setupLifecycleObservers() {
|
|
109
|
+
NotificationCenter.default.addObserver(
|
|
110
|
+
self,
|
|
111
|
+
selector: #selector(handleWillEnterForeground),
|
|
112
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
113
|
+
object: nil
|
|
114
|
+
)
|
|
115
|
+
NotificationCenter.default.addObserver(
|
|
116
|
+
self,
|
|
117
|
+
selector: #selector(handleDidEnterBackground),
|
|
118
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
119
|
+
object: nil
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@objc
|
|
124
|
+
private func handleWillEnterForeground() {
|
|
125
|
+
// iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
|
|
126
|
+
// a state update on foreground so shimmer reliably restarts when returning to the app.
|
|
127
|
+
isAppActive = true
|
|
128
|
+
updateLayersForCurrentState()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@objc
|
|
132
|
+
private func handleDidEnterBackground() {
|
|
133
|
+
isAppActive = false
|
|
134
|
+
stopAnimation()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func updateLayersForCurrentState() {
|
|
138
|
+
let bounds = self.bounds
|
|
139
|
+
guard !bounds.isEmpty else {
|
|
140
|
+
stopAnimation()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
baseLayer.frame = bounds
|
|
145
|
+
baseLayer.backgroundColor = baseColor.cgColor
|
|
146
|
+
|
|
147
|
+
updateShimmerLayer(for: bounds)
|
|
148
|
+
updateShimmerAnimation(for: bounds)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func updateShimmerLayer(for bounds: CGRect) {
|
|
152
|
+
// Rebuild the shimmer gradient for current width/colors. Keep this tied to real state changes
|
|
153
|
+
// such as layout/prop updates, not continuous per frame calls.
|
|
154
|
+
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
155
|
+
let transparentHighlight = color(gradientColor, alphaFactor: 0)
|
|
156
|
+
shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height)
|
|
157
|
+
shimmerLayer.colors = [
|
|
158
|
+
transparentHighlight.cgColor,
|
|
159
|
+
color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
|
|
160
|
+
color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
|
|
161
|
+
color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
|
|
162
|
+
color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
|
|
163
|
+
gradientColor.cgColor,
|
|
164
|
+
color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
|
|
165
|
+
color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
|
|
166
|
+
color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
|
|
167
|
+
color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
|
|
168
|
+
transparentHighlight.cgColor,
|
|
169
|
+
]
|
|
170
|
+
shimmerLayer.isHidden = !enabled
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func updateShimmerAnimation(for bounds: CGRect) {
|
|
174
|
+
guard enabled, isAppActive, window != nil, bounds.width > 0, bounds.height > 0 else {
|
|
175
|
+
stopAnimation()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If an animation already exists for the same size, keep it running instead of restarting.
|
|
180
|
+
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
|
|
181
|
+
lastAnimatedSize == bounds.size,
|
|
182
|
+
lastAnimatedDuration == shimmerDuration
|
|
183
|
+
{
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
stopAnimation()
|
|
188
|
+
|
|
189
|
+
// Start just outside the left edge and sweep fully past the right edge for a clean pass.
|
|
190
|
+
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
191
|
+
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
|
192
|
+
animation.fromValue = 0
|
|
193
|
+
animation.toValue = bounds.width + shimmerWidth
|
|
194
|
+
animation.duration = shimmerDuration
|
|
195
|
+
animation.repeatCount = .infinity
|
|
196
|
+
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
197
|
+
animation.isRemovedOnCompletion = true
|
|
198
|
+
shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey)
|
|
199
|
+
lastAnimatedDuration = shimmerDuration
|
|
200
|
+
lastAnimatedSize = bounds.size
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static func normalizedDuration(milliseconds: Double) -> CFTimeInterval {
|
|
204
|
+
guard milliseconds > 0 else { return defaultShimmerDuration }
|
|
205
|
+
return milliseconds / 1000
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
|
|
209
|
+
// Preserve the resolved color channels and shape only alpha for smooth highlight falloff.
|
|
210
|
+
let resolvedColor = color.resolvedColor(with: traitCollection)
|
|
211
|
+
|
|
212
|
+
var red: CGFloat = 0
|
|
213
|
+
var green: CGFloat = 0
|
|
214
|
+
var blue: CGFloat = 0
|
|
215
|
+
var alpha: CGFloat = 0
|
|
216
|
+
|
|
217
|
+
if resolvedColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
|
218
|
+
return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
guard
|
|
222
|
+
let converted = resolvedColor.cgColor.converted(
|
|
223
|
+
to: CGColorSpace(name: CGColorSpace.extendedSRGB)!,
|
|
224
|
+
intent: .defaultIntent,
|
|
225
|
+
options: nil
|
|
226
|
+
),
|
|
227
|
+
let components = converted.components
|
|
228
|
+
else {
|
|
229
|
+
return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
switch components.count {
|
|
233
|
+
case 2:
|
|
234
|
+
return UIColor(
|
|
235
|
+
white: components[0],
|
|
236
|
+
alpha: components[1] * alphaFactor
|
|
237
|
+
)
|
|
238
|
+
case 4:
|
|
239
|
+
return UIColor(
|
|
240
|
+
red: components[0],
|
|
241
|
+
green: components[1],
|
|
242
|
+
blue: components[2],
|
|
243
|
+
alpha: components[3] * alphaFactor
|
|
244
|
+
)
|
|
245
|
+
default:
|
|
246
|
+
return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
#import <React/RCTViewComponentView.h>
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
#ifndef StreamShimmerViewComponentView_h
|
|
8
|
+
#define StreamShimmerViewComponentView_h
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
11
|
+
|
|
12
|
+
@interface StreamShimmerViewComponentView :
|
|
13
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
14
|
+
RCTViewComponentView
|
|
15
|
+
#else
|
|
16
|
+
UIView
|
|
17
|
+
#endif
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
NS_ASSUME_NONNULL_END
|
|
21
|
+
|
|
22
|
+
#endif /* StreamShimmerViewComponentView_h */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#import "StreamShimmerViewComponentView.h"
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
|
|
5
|
+
#if __has_include(<react/renderer/components/StreamChatReactNativeSpec/ComponentDescriptors.h>)
|
|
6
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/ComponentDescriptors.h>
|
|
7
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/Props.h>
|
|
8
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/RCTComponentViewHelpers.h>
|
|
9
|
+
#elif __has_include(<react/renderer/components/StreamChatExpoSpec/ComponentDescriptors.h>)
|
|
10
|
+
#import <react/renderer/components/StreamChatExpoSpec/ComponentDescriptors.h>
|
|
11
|
+
#import <react/renderer/components/StreamChatExpoSpec/Props.h>
|
|
12
|
+
#import <react/renderer/components/StreamChatExpoSpec/RCTComponentViewHelpers.h>
|
|
13
|
+
#else
|
|
14
|
+
#error "Unable to find generated codegen headers for StreamShimmerView."
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
#if __has_include(<stream_chat_react_native/stream_chat_react_native-Swift.h>)
|
|
18
|
+
#import <stream_chat_react_native/stream_chat_react_native-Swift.h>
|
|
19
|
+
#elif __has_include(<stream_chat_expo/stream_chat_expo-Swift.h>)
|
|
20
|
+
#import <stream_chat_expo/stream_chat_expo-Swift.h>
|
|
21
|
+
#elif __has_include("stream_chat_react_native-Swift.h")
|
|
22
|
+
#import "stream_chat_react_native-Swift.h"
|
|
23
|
+
#elif __has_include("stream_chat_expo-Swift.h")
|
|
24
|
+
#import "stream_chat_expo-Swift.h"
|
|
25
|
+
#else
|
|
26
|
+
#error "Unable to import generated Swift header for StreamShimmerView."
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
#import <React/RCTConversions.h>
|
|
30
|
+
|
|
31
|
+
using namespace facebook::react;
|
|
32
|
+
|
|
33
|
+
@interface StreamShimmerViewComponentView () <RCTStreamShimmerViewViewProtocol>
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
// Fabric bridge for StreamShimmerView. This component view owns the native shimmer instance,
|
|
37
|
+
// applies codegen props, and keeps shimmer rendered as a background layer while Fabric manages
|
|
38
|
+
// React children. Keeping shimmer as a layer avoids child-order conflicts during mount/unmount.
|
|
39
|
+
@implementation StreamShimmerViewComponentView {
|
|
40
|
+
StreamShimmerView *_shimmerView;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
44
|
+
{
|
|
45
|
+
return concreteComponentDescriptorProvider<StreamShimmerViewComponentDescriptor>();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
49
|
+
{
|
|
50
|
+
if (self = [super initWithFrame:frame]) {
|
|
51
|
+
static const auto defaultProps = std::make_shared<const StreamShimmerViewProps>();
|
|
52
|
+
_props = defaultProps;
|
|
53
|
+
|
|
54
|
+
_shimmerView = [[StreamShimmerView alloc] initWithFrame:self.bounds];
|
|
55
|
+
_shimmerView.userInteractionEnabled = NO;
|
|
56
|
+
[self.layer insertSublayer:_shimmerView.layer atIndex:0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return self;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (void)layoutSubviews
|
|
63
|
+
{
|
|
64
|
+
[super layoutSubviews];
|
|
65
|
+
_shimmerView.frame = self.bounds;
|
|
66
|
+
|
|
67
|
+
// Keep shimmer pinned as the layer furthest back. Some layer operations can reorder sublayers, and
|
|
68
|
+
// this guard restores expected layering without touching Fabric managed child views.
|
|
69
|
+
BOOL needsReinsert = _shimmerView.layer.superlayer != self.layer;
|
|
70
|
+
if (!needsReinsert) {
|
|
71
|
+
CALayer *firstLayer = self.layer.sublayers.firstObject;
|
|
72
|
+
needsReinsert = firstLayer != _shimmerView.layer;
|
|
73
|
+
}
|
|
74
|
+
if (needsReinsert) {
|
|
75
|
+
[self.layer insertSublayer:_shimmerView.layer atIndex:0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
80
|
+
{
|
|
81
|
+
const auto &newProps = *std::static_pointer_cast<const StreamShimmerViewProps>(props);
|
|
82
|
+
|
|
83
|
+
UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor colorWithWhite:1 alpha:0];
|
|
84
|
+
UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor];
|
|
85
|
+
|
|
86
|
+
[_shimmerView applyWithBaseColor:baseColor
|
|
87
|
+
gradientColor:gradientColor
|
|
88
|
+
durationMilliseconds:newProps.duration
|
|
89
|
+
enabled:newProps.enabled];
|
|
90
|
+
|
|
91
|
+
[super updateProps:props oldProps:oldProps];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (void)prepareForRecycle
|
|
95
|
+
{
|
|
96
|
+
[super prepareForRecycle];
|
|
97
|
+
// Defensive cleanup for recycled cells/views so offscreen instances do not keep animating.
|
|
98
|
+
[_shimmerView stopAnimation];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)dealloc
|
|
102
|
+
{
|
|
103
|
+
[_shimmerView stopAnimation];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@end
|
|
107
|
+
|
|
108
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-chat-react-native",
|
|
3
3
|
"description": "The official React Native SDK for Stream Chat, a service for building chat applications",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "9.0.0-beta.1",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/stream-chat-react-native",
|
|
6
6
|
"author": {
|
|
7
7
|
"company": "Stream.io Inc",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
17
|
"types",
|
|
18
|
-
"android",
|
|
18
|
+
"android/src",
|
|
19
|
+
"android/build.gradle",
|
|
20
|
+
"android/gradle.properties",
|
|
21
|
+
"android/gradle",
|
|
19
22
|
"ios",
|
|
20
23
|
"*.podspec",
|
|
21
24
|
"package.json"
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"es6-symbol": "^3.1.3",
|
|
28
31
|
"mime": "^4.0.7",
|
|
29
|
-
"stream-chat-react-native-core": "
|
|
32
|
+
"stream-chat-react-native-core": "9.0.0-beta.1"
|
|
30
33
|
},
|
|
31
34
|
"peerDependencies": {
|
|
32
35
|
"@react-native-camera-roll/camera-roll": ">=7.8.0",
|
|
@@ -78,16 +81,22 @@
|
|
|
78
81
|
}
|
|
79
82
|
},
|
|
80
83
|
"scripts": {
|
|
81
|
-
"
|
|
82
|
-
"
|
|
84
|
+
"postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi",
|
|
85
|
+
"prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .",
|
|
86
|
+
"postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package"
|
|
83
87
|
},
|
|
84
88
|
"devDependencies": {
|
|
85
89
|
"react-native": "^0.79.3"
|
|
86
90
|
},
|
|
87
91
|
"codegenConfig": {
|
|
88
92
|
"name": "StreamChatReactNativeSpec",
|
|
89
|
-
"type": "
|
|
90
|
-
"jsSrcsDir": "src/native"
|
|
93
|
+
"type": "all",
|
|
94
|
+
"jsSrcsDir": "src/native",
|
|
95
|
+
"ios": {
|
|
96
|
+
"componentProvider": {
|
|
97
|
+
"StreamShimmerView": "StreamShimmerViewComponentView"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
91
100
|
},
|
|
92
101
|
"resolutions": {
|
|
93
102
|
"@types/react": "^19.0.0"
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getLocalAssetUri,
|
|
12
12
|
getPhotos,
|
|
13
13
|
iOS14RefreshGallerySelection,
|
|
14
|
+
NativeShimmerView,
|
|
14
15
|
oniOS14GalleryLibrarySelectionChange,
|
|
15
16
|
overrideAudioRecordingConfiguration,
|
|
16
17
|
pickDocument,
|
|
@@ -32,6 +33,7 @@ registerNativeHandlers({
|
|
|
32
33
|
getLocalAssetUri,
|
|
33
34
|
getPhotos,
|
|
34
35
|
iOS14RefreshGallerySelection,
|
|
36
|
+
NativeShimmerView,
|
|
35
37
|
oniOS14GalleryLibrarySelectionChange,
|
|
36
38
|
overrideAudioRecordingConfiguration,
|
|
37
39
|
pickDocument,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ColorValue, HostComponent, ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
5
|
+
|
|
6
|
+
export interface NativeProps extends ViewProps {
|
|
7
|
+
baseColor?: ColorValue;
|
|
8
|
+
duration?: WithDefault<Int32, 1200>;
|
|
9
|
+
enabled?: WithDefault<boolean, true>;
|
|
10
|
+
gradientColor?: ColorValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default codegenNativeComponent<NativeProps>(
|
|
14
|
+
'StreamShimmerView',
|
|
15
|
+
) as HostComponent<NativeProps>;
|
|
@@ -2,7 +2,19 @@ import React from 'react';
|
|
|
2
2
|
import AudioVideoPlayer from './AudioVideo';
|
|
3
3
|
|
|
4
4
|
export const Video = AudioVideoPlayer
|
|
5
|
-
? ({
|
|
5
|
+
? ({
|
|
6
|
+
onBuffer,
|
|
7
|
+
onEnd,
|
|
8
|
+
onLoad,
|
|
9
|
+
onProgress,
|
|
10
|
+
paused,
|
|
11
|
+
repeat,
|
|
12
|
+
resizeMode,
|
|
13
|
+
style,
|
|
14
|
+
uri,
|
|
15
|
+
videoRef,
|
|
16
|
+
rate,
|
|
17
|
+
}) => (
|
|
6
18
|
<AudioVideoPlayer
|
|
7
19
|
ignoreSilentSwitch={'ignore'}
|
|
8
20
|
onBuffer={onBuffer}
|
|
@@ -20,6 +32,7 @@ export const Video = AudioVideoPlayer
|
|
|
20
32
|
uri,
|
|
21
33
|
}}
|
|
22
34
|
style={style}
|
|
35
|
+
rate={rate}
|
|
23
36
|
/>
|
|
24
37
|
)
|
|
25
38
|
: null;
|
|
@@ -4,6 +4,7 @@ export * from './FlatList';
|
|
|
4
4
|
export * from './getLocalAssetUri';
|
|
5
5
|
export * from './getPhotos';
|
|
6
6
|
export * from './iOS14RefreshGallerySelection';
|
|
7
|
+
export * from './NativeShimmerView';
|
|
7
8
|
export * from './oniOS14GalleryLibrarySelectionChange';
|
|
8
9
|
export * from './pickDocument';
|
|
9
10
|
export * from './pickImage';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
-
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
4
|
|
|
6
5
|
Pod::Spec.new do |s|
|
|
7
6
|
s.name = "stream-chat-react-native"
|
|
@@ -11,32 +10,16 @@ Pod::Spec.new do |s|
|
|
|
11
10
|
s.license = package["license"]
|
|
12
11
|
s.authors = package["author"]
|
|
13
12
|
|
|
14
|
-
s.platforms = { :ios =>
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
15
14
|
s.source = { :git => "./ios", :tag => "#{s.version}" }
|
|
15
|
+
s.prepare_command = <<-CMD
|
|
16
|
+
if [ -d ../shared-native/ios ]; then
|
|
17
|
+
mkdir -p ios/shared
|
|
18
|
+
rsync -a --delete ../shared-native/ios/ ios/shared/
|
|
19
|
+
fi
|
|
20
|
+
CMD
|
|
21
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
22
|
+
s.private_header_files = "ios/**/*.h"
|
|
16
23
|
|
|
17
|
-
s
|
|
18
|
-
|
|
19
|
-
s.dependency "React-Core"
|
|
20
|
-
s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
|
|
21
|
-
|
|
22
|
-
# Don't install the dependencies when we run `pod install` in the old architecture.
|
|
23
|
-
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
|
24
|
-
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
25
|
-
s.pod_target_xcconfig = {
|
|
26
|
-
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
27
|
-
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
28
|
-
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
|
29
|
-
}
|
|
30
|
-
s.dependency "React-Codegen"
|
|
31
|
-
# Prebuilt RN modes already handle Folly (or don't expose RCT-Folly podspec).
|
|
32
|
-
use_prebuilt_core = ENV['RCT_USE_PREBUILT_RNCORE'] == '1'
|
|
33
|
-
use_prebuilt_deps = ENV['RCT_USE_RN_DEP'] == '1'
|
|
34
|
-
unless use_prebuilt_core || use_prebuilt_deps
|
|
35
|
-
s.dependency "RCT-Folly"
|
|
36
|
-
end
|
|
37
|
-
s.dependency "RCTRequired"
|
|
38
|
-
s.dependency "RCTTypeSafety"
|
|
39
|
-
s.dependency "ReactCommon/turbomodule/core"
|
|
40
|
-
install_modules_dependencies(s)
|
|
41
|
-
end
|
|
24
|
+
install_modules_dependencies(s)
|
|
42
25
|
end
|