stream-chat-expo 9.9.1 → 9.9.2-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.
|
@@ -3,6 +3,8 @@ import UIKit
|
|
|
3
3
|
|
|
4
4
|
private protocol StreamShimmerAppLifecycleObserving: AnyObject {
|
|
5
5
|
func shimmerAppLifecycleDidChange(isActive: Bool)
|
|
6
|
+
/// A scene changed activation state; observers re-evaluate their own.
|
|
7
|
+
func shimmerSceneLifecycleDidChange()
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
private final class StreamShimmerAppLifecycleCoordinator: NSObject {
|
|
@@ -28,6 +30,21 @@ private final class StreamShimmerAppLifecycleCoordinator: NSObject {
|
|
|
28
30
|
name: UIApplication.didEnterBackgroundNotification,
|
|
29
31
|
object: nil
|
|
30
32
|
)
|
|
33
|
+
|
|
34
|
+
// A scene can change activation state with no app-level transition (Split View).
|
|
35
|
+
for name in [
|
|
36
|
+
UIScene.didActivateNotification,
|
|
37
|
+
UIScene.willDeactivateNotification,
|
|
38
|
+
UIScene.didEnterBackgroundNotification,
|
|
39
|
+
UIScene.willEnterForegroundNotification,
|
|
40
|
+
] {
|
|
41
|
+
notificationCenter.addObserver(
|
|
42
|
+
self,
|
|
43
|
+
selector: #selector(handleSceneLifecycleChange),
|
|
44
|
+
name: name,
|
|
45
|
+
object: nil
|
|
46
|
+
)
|
|
47
|
+
}
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
func addObserver(_ observer: StreamShimmerAppLifecycleObserving) {
|
|
@@ -49,6 +66,13 @@ private final class StreamShimmerAppLifecycleCoordinator: NSObject {
|
|
|
49
66
|
broadcastAppState(isActive: false)
|
|
50
67
|
}
|
|
51
68
|
|
|
69
|
+
@objc
|
|
70
|
+
private func handleSceneLifecycleChange() {
|
|
71
|
+
for case let observer as StreamShimmerAppLifecycleObserving in observers.allObjects {
|
|
72
|
+
observer.shimmerSceneLifecycleDidChange()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
private func broadcastAppState(isActive: Bool) {
|
|
53
77
|
self.isAppActive = isActive
|
|
54
78
|
|
|
@@ -83,8 +107,35 @@ public final class StreamShimmerView: UIView {
|
|
|
83
107
|
private static let shimmerAnimationKey = "stream_shimmer_translate_x"
|
|
84
108
|
private static let gradientLocations: [NSNumber] = [0.0, 0.35, 0.5, 0.65, 1.0]
|
|
85
109
|
private static let gradientAlphaFactors: [CGFloat] = [0, softHighlightAlpha, 1, softHighlightAlpha, 0]
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
/// Scale of the display this view is on. Not `UIScreen.main`: an iPhone app is resizable from
|
|
111
|
+
/// iOS 27 and may be on a secondary display. Reports 0 before the view has a window.
|
|
112
|
+
private var currentDisplayScale: CGFloat {
|
|
113
|
+
let traitScale = traitCollection.displayScale
|
|
114
|
+
if traitScale > 0 {
|
|
115
|
+
return traitScale
|
|
116
|
+
}
|
|
117
|
+
return window?.windowScene?.screen.scale ?? 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private var animationDistanceTolerance: CGFloat {
|
|
121
|
+
1 / max(currentDisplayScale, 1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Whether this view's scene is on screen. Preferred over the app-wide `applicationState`, which
|
|
125
|
+
/// cannot distinguish scenes in Split View. `foregroundInactive` counts as on screen: that is the
|
|
126
|
+
/// visible-but-unfocused state a side-by-side scene reports.
|
|
127
|
+
private var isSceneOnScreen: Bool {
|
|
128
|
+
guard let activationState = window?.windowScene?.activationState else {
|
|
129
|
+
return isAppActive
|
|
130
|
+
}
|
|
131
|
+
switch activationState {
|
|
132
|
+
case .foregroundActive, .foregroundInactive:
|
|
133
|
+
return true
|
|
134
|
+
case .background, .unattached:
|
|
135
|
+
return false
|
|
136
|
+
@unknown default:
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
private let baseLayer = CALayer()
|
|
@@ -140,14 +191,17 @@ public final class StreamShimmerView: UIView {
|
|
|
140
191
|
// a later reattach starts from a clean state.
|
|
141
192
|
stopAnimation()
|
|
142
193
|
} else {
|
|
143
|
-
//
|
|
144
|
-
|
|
194
|
+
// The new window may be on a display with a different scale.
|
|
195
|
+
syncContentsScale()
|
|
145
196
|
updateLayersForCurrentState()
|
|
146
197
|
}
|
|
147
198
|
}
|
|
148
199
|
|
|
149
200
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
150
201
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
202
|
+
if traitCollection.displayScale != previousTraitCollection?.displayScale {
|
|
203
|
+
syncContentsScale()
|
|
204
|
+
}
|
|
151
205
|
if let previousTraitCollection,
|
|
152
206
|
traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
|
|
153
207
|
{
|
|
@@ -194,10 +248,16 @@ public final class StreamShimmerView: UIView {
|
|
|
194
248
|
lastAnimatedTravelDistance = 0
|
|
195
249
|
}
|
|
196
250
|
|
|
251
|
+
private func syncContentsScale() {
|
|
252
|
+
let scale = currentDisplayScale
|
|
253
|
+
guard shimmerLayer.contentsScale != scale else { return }
|
|
254
|
+
shimmerLayer.contentsScale = scale
|
|
255
|
+
}
|
|
256
|
+
|
|
197
257
|
private func setupLayers() {
|
|
198
258
|
isUserInteractionEnabled = false
|
|
199
259
|
|
|
200
|
-
shimmerLayer.contentsScale =
|
|
260
|
+
shimmerLayer.contentsScale = currentDisplayScale
|
|
201
261
|
shimmerLayer.allowsEdgeAntialiasing = true
|
|
202
262
|
shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
203
263
|
shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
@@ -249,7 +309,7 @@ public final class StreamShimmerView: UIView {
|
|
|
249
309
|
private func updateShimmerAnimation(for bounds: CGRect) {
|
|
250
310
|
guard
|
|
251
311
|
enabled,
|
|
252
|
-
|
|
312
|
+
isSceneOnScreen,
|
|
253
313
|
window != nil,
|
|
254
314
|
!isHidden,
|
|
255
315
|
alpha > 0.01,
|
|
@@ -267,7 +327,7 @@ public final class StreamShimmerView: UIView {
|
|
|
267
327
|
// restarting. Fabric can relayout the view for height-only or subpixel changes that do not
|
|
268
328
|
// require a new horizontal sweep.
|
|
269
329
|
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
|
|
270
|
-
abs(lastAnimatedTravelDistance - animationTravelDistance) <=
|
|
330
|
+
abs(lastAnimatedTravelDistance - animationTravelDistance) <= animationDistanceTolerance,
|
|
271
331
|
lastAnimatedDuration == shimmerDuration
|
|
272
332
|
{
|
|
273
333
|
return
|
|
@@ -339,14 +399,22 @@ public final class StreamShimmerView: UIView {
|
|
|
339
399
|
}
|
|
340
400
|
|
|
341
401
|
extension StreamShimmerView: StreamShimmerAppLifecycleObserving {
|
|
342
|
-
func
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
402
|
+
func shimmerSceneLifecycleDidChange() {
|
|
403
|
+
reevaluateForSceneState()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private func reevaluateForSceneState() {
|
|
407
|
+
if isSceneOnScreen {
|
|
347
408
|
updateLayersForCurrentState()
|
|
348
409
|
} else {
|
|
349
410
|
stopAnimation()
|
|
350
411
|
}
|
|
351
412
|
}
|
|
413
|
+
|
|
414
|
+
func shimmerAppLifecycleDidChange(isActive: Bool) {
|
|
415
|
+
// iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
|
|
416
|
+
// a state update on foreground so shimmer reliably restarts when returning to the app.
|
|
417
|
+
self.isAppActive = isActive
|
|
418
|
+
reevaluateForSceneState()
|
|
419
|
+
}
|
|
352
420
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-chat-expo",
|
|
3
3
|
"description": "The official Expo SDK for Stream Chat, a service for building chat applications",
|
|
4
|
-
"version": "9.9.1",
|
|
4
|
+
"version": "9.9.2-beta.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"company": "Stream.io Inc",
|
|
7
7
|
"name": "Stream.io Inc"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"types": "types/index.d.ts",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"mime": "^4.0.7",
|
|
30
|
-
"stream-chat-react-native-core": "9.9.1"
|
|
30
|
+
"stream-chat-react-native-core": "9.9.2-beta.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"expo": ">=52.0.0",
|