diff --git a/.gitignore b/.gitignore
index 5ecbf803c8..3895765f2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ index.android.bundle
*.app
*.DSYM.zip
**/metrics/
+package/shared-native/.sync-state/
diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json
index 05cdda1247..3fc4d38da1 100644
--- a/examples/ExpoMessaging/package.json
+++ b/examples/ExpoMessaging/package.json
@@ -3,6 +3,10 @@
"version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
+ "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh expo-package && bash ../../package/scripts/sync-shared-native.sh expo-package",
+ "prestart": "yarn sync-native",
+ "preandroid": "yarn sync-native",
+ "preios": "yarn sync-native",
"start": "expo start --dev-client",
"android": "expo run:android",
"ios": "expo run:ios",
@@ -44,6 +48,7 @@
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
+ "react-native-teleport": "^1.0.2",
"react-native-worklets": "0.5.1",
"stream-chat-expo": "link:../../package/expo-package",
"stream-chat-react-native-core": "link:../../package"
diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock
index 104da149ca..c74a42d065 100644
--- a/examples/ExpoMessaging/yarn.lock
+++ b/examples/ExpoMessaging/yarn.lock
@@ -1533,7 +1533,15 @@
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz#9d5b4b6f23309260a12856cb574c5e64e6c133f7"
integrity sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ==
-"@gorhom/bottom-sheet@^5.1.6", "@gorhom/bottom-sheet@^5.1.8":
+"@gorhom/bottom-sheet@5.1.8":
+ version "5.1.8"
+ resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.1.8.tgz#65547917f5b1dae5a1291dabd4ea8bfee09feba4"
+ integrity sha512-QuYIVjn3K9bW20n5bgOSjvxBYoWG4YQXiLGOheEAMgISuoT6sMcA270ViSkkb0fenPxcIOwzCnFNuxmr739T9A==
+ dependencies:
+ "@gorhom/portal" "1.0.14"
+ invariant "^2.2.4"
+
+"@gorhom/bottom-sheet@^5.1.6":
version "5.2.6"
resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-5.2.6.tgz#5f2045f6ca965383afe39f7dfa3afad1502b7467"
integrity sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ==
@@ -4256,22 +4264,6 @@ json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
-jsonwebtoken@^9.0.2:
- version "9.0.2"
- resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
- integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
- dependencies:
- jws "^3.2.2"
- lodash.includes "^4.3.0"
- lodash.isboolean "^3.0.3"
- lodash.isinteger "^4.0.4"
- lodash.isnumber "^3.0.3"
- lodash.isplainobject "^4.0.6"
- lodash.isstring "^4.0.1"
- lodash.once "^4.0.0"
- ms "^2.1.1"
- semver "^7.5.4"
-
jsonwebtoken@^9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
@@ -4288,15 +4280,6 @@ jsonwebtoken@^9.0.3:
ms "^2.1.1"
semver "^7.5.4"
-jwa@^1.4.1:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
- integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
- dependencies:
- buffer-equal-constant-time "^1.0.1"
- ecdsa-sig-formatter "1.0.11"
- safe-buffer "^5.0.1"
-
jwa@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
@@ -4306,14 +4289,6 @@ jwa@^2.0.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
-jws@^3.2.2:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
- integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
- dependencies:
- jwa "^1.4.1"
- safe-buffer "^5.0.1"
-
jws@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
@@ -4448,6 +4423,11 @@ lodash-es@4.17.21:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+lodash-es@4.17.23:
+ version "4.17.23"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
+ integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
+
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -5555,6 +5535,11 @@ react-native-svg@15.12.1:
css-tree "^1.1.3"
warn-once "0.1.1"
+react-native-teleport@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/react-native-teleport/-/react-native-teleport-1.0.2.tgz#f5bed0534acba29787a6e3707513eed91cb4f8ea"
+ integrity sha512-+DE9N9JMxulUZwREDPBYl10Urmqocvzw++/BXzC34YzaHaDfbmgvr/KFJjGYoZhJUMcOJjBC9OxESH6+yzvxJA==
+
react-native-url-polyfill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589"
@@ -6052,10 +6037,10 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""
-stream-chat@^9.27.2:
- version "9.27.2"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
- integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
+stream-chat@^9.35.1:
+ version "9.35.1"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.35.1.tgz#d828854a9c27ea7e45e6642d9107966c6606f552"
+ integrity sha512-649sgO7+llFuW+y/Ja0K4d94aUC+EMxYUVo5mq5AFGT86vUAIXmRIMVHYHA/jw4MYoqfWAFrDK6L9Rhyn/eMkQ==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -6068,9 +6053,9 @@ stream-chat@^9.27.2:
ws "^8.18.1"
stream-chat@^9.9.0:
- version "9.20.3"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.20.3.tgz#5f47d6f46d146202c743282f5fb7350f4a640922"
- integrity sha512-206Lea0ZAVWbfYZkIwLG5m+++ELD3f8EAEL/YzbMDL++E2vU2WhQ2d1HNb1ROXURZUF0Sy845htTw1rwnahomw==
+ version "9.36.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.36.0.tgz#154e0d6bdf8b15e97a6d9718c655d2ede34f6f25"
+ integrity sha512-D1b5THI4UbnvsEcJyUv1tUIgK6lCYT+aStrV+87mdrM9owX+WUpKaWFkxz/Ug+DOrJtTazvfuzvpJMyDi82NXA==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -6078,7 +6063,7 @@ stream-chat@^9.9.0:
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
- jsonwebtoken "^9.0.2"
+ jsonwebtoken "^9.0.3"
linkifyjs "^4.3.2"
ws "^8.18.1"
diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile
index 0efb03543f..365d3534ee 100644
--- a/examples/SampleApp/fastlane/Fastfile
+++ b/examples/SampleApp/fastlane/Fastfile
@@ -184,6 +184,7 @@ lane :frameworks_sizes do
yarn_all
sh('yarn build')
sh('yarn minify-bundle')
+
js_bundle_size = file_size(path: 'package/lib/module/bundle.min.js')
{ js_bundle_size: js_bundle_size }
end
diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock
index 61fc07eb9c..acad889774 100644
--- a/examples/SampleApp/ios/Podfile.lock
+++ b/examples/SampleApp/ios/Podfile.lock
@@ -289,7 +289,6 @@ PODS:
- React-RCTText (= 0.80.2)
- React-RCTVibration (= 0.80.2)
- React-callinvoker (0.80.2)
- - React-Codegen (0.1.0)
- React-Core (0.80.2):
- boost
- DoubleConversion
@@ -1920,6 +1919,35 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - react-native-blur (4.4.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- react-native-cameraroll (7.10.0):
- boost
- DoubleConversion
@@ -2722,6 +2750,35 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - RNCClipboard (1.16.3):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- RNFastImage (8.6.3):
- React-Core
- SDWebImage (~> 5.11.1)
@@ -3146,7 +3203,6 @@ PODS:
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- - React-Codegen
- React-Core
- React-debug
- React-Fabric
@@ -3275,6 +3331,7 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
+ - "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
@@ -3315,6 +3372,7 @@ DEPENDENCIES:
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
+ - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- RNFastImage (from `../node_modules/react-native-fast-image`)
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
@@ -3351,7 +3409,6 @@ SPEC REPOS:
- nanopb
- PromisesObjC
- PromisesSwift
- - React-Codegen
- SDWebImage
- SDWebImageWebPCoder
- SocketRocket
@@ -3446,6 +3503,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
+ react-native-blur:
+ :path: "../node_modules/@react-native-community/blur"
react-native-cameraroll:
:path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-document-picker:
@@ -3526,6 +3585,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
+ RNCClipboard:
+ :path: "../node_modules/@react-native-clipboard/clipboard"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNFBApp:
@@ -3584,13 +3645,12 @@ SPEC CHECKSUMS:
op-sqlite: a7e46cfdaebeef219fd0e939332967af9fe6d406
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
- RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
+ RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7
RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4
RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f
React: e7a4655b09d0e17e54be188cc34c2f3e2087318a
React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b
- React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a
React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542
React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553
React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691
@@ -3619,6 +3679,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: 8f620d1794c6b59a8c3862c3ae820a2e9e6c9bb0
React-microtasksnativemodule: dcf5321c9a41659a6718df8a5f202af1577c6825
react-native-blob-util: a511afccff6511544ebf56928e6afdf837b037a7
+ react-native-blur: ecdc987ab8d8fba95abef14551f033376872d0a6
react-native-cameraroll: 8c3ba9b6f511cf645778de19d5039b61d922fdfb
react-native-document-picker: b37cf6660ad9087b782faa78a1e67687fac15bfd
react-native-geolocation: b7f68b8c04e36ee669c630dbc48dd42cf93a0a41
@@ -3659,6 +3720,7 @@ SPEC CHECKSUMS:
ReactCodegen: 4928682e20747464165effacc170019a18da953c
ReactCommon: ec1cdf708729338070f8c4ad746768a782fd9eb1
RNCAsyncStorage: f30b3a83064e28b0fc46f1fbd3834589ed64c7b9
+ RNCClipboard: 8e5237c79dafacea5b7adf4c3ab39a4236b5ef7e
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e
RNFBMessaging: de62448d205095171915d622ed5fb45c2be5e075
@@ -3673,7 +3735,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- stream-chat-react-native: f42e234640869e0eafcdd354441414ad1818b9fe
+ stream-chat-react-native: 3a5d663e1d32afb54a3afba3691f08be65a20374
Teleport: c089481dd2bb020e3dced39b7f8849b93d1499f6
Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed
diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json
index 20836e2dc4..0a3b243ecf 100644
--- a/examples/SampleApp/package.json
+++ b/examples/SampleApp/package.json
@@ -7,8 +7,12 @@
"url": "https://github.com/GetStream/stream-chat-react-native.git"
},
"scripts": {
+ "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh native-package && bash ../../package/scripts/sync-shared-native.sh native-package",
+ "preandroid": "yarn sync-native",
"android": "react-native run-android",
+ "preios": "yarn sync-native",
"ios": "react-native run-ios",
+ "prestart": "yarn sync-native",
"start": "react-native start",
"test": "jest",
"lint": "eslint .",
diff --git a/examples/TypeScriptMessaging/package.json b/examples/TypeScriptMessaging/package.json
index 3f9e55baae..c2efffc829 100644
--- a/examples/TypeScriptMessaging/package.json
+++ b/examples/TypeScriptMessaging/package.json
@@ -3,8 +3,12 @@
"version": "0.0.1",
"private": true,
"scripts": {
+ "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh native-package && bash ../../package/scripts/sync-shared-native.sh native-package",
+ "preandroid": "yarn sync-native",
"android": "react-native run-android",
+ "preios": "yarn sync-native",
"ios": "react-native run-ios",
+ "prestart": "yarn sync-native",
"start": "react-native start",
"test": "jest",
"lint": "eslint .",
diff --git a/package/expo-package/.gitignore b/package/expo-package/.gitignore
new file mode 100644
index 0000000000..81e0235dce
--- /dev/null
+++ b/package/expo-package/.gitignore
@@ -0,0 +1,7 @@
+# android
+android/build
+android/src/main/java/com/streamchatreactnative/shared
+
+# ios
+ios/build
+ios/shared
diff --git a/package/expo-package/android/build.gradle b/package/expo-package/android/build.gradle
new file mode 100644
index 0000000000..d64a081fba
--- /dev/null
+++ b/package/expo-package/android/build.gradle
@@ -0,0 +1,139 @@
+def kotlinVersion =
+ rootProject.ext.has("kotlinVersion")
+ ? rootProject.ext.get("kotlinVersion")
+ : project.properties["StreamChatExpo_kotlinVersion"]
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.2.1"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+ }
+}
+
+def isNewArchitectureEnabled() {
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
+}
+
+apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
+
+if (isNewArchitectureEnabled()) {
+ apply plugin: "com.facebook.react"
+}
+
+def getExtOrIntegerDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger()
+}
+def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
+def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
+def hasNativeSources = { File dir ->
+ dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
+}
+
+android {
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+ def agpMajorVersion = agpVersion.tokenize('.')[0].toInteger()
+ def agpMinorVersion = agpVersion.tokenize('.')[1].toInteger()
+
+ if (agpMajorVersion >= 7 && agpMinorVersion >= 3) {
+ namespace "com.streamchatexpo"
+ }
+
+ defaultConfig {
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ lintOptions {
+ disable "GradleCompatible"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ sourceSets {
+ main {
+ if (isNewArchitectureEnabled()) {
+ java.srcDirs += [
+ "src/newarch",
+ "src/main/java/com/streamchatreactnative/shared",
+ "${project.buildDir}/generated/source/codegen/java"
+ ]
+ }
+ }
+ }
+}
+
+tasks.register("syncSharedShimmerSources") {
+ outputs.dir(localSharedNativeRootDir)
+ outputs.upToDateWhen { false }
+ doLast {
+ def sourceRootDir = null
+ if (hasNativeSources(localSharedNativeRootDir)) {
+ sourceRootDir = localSharedNativeRootDir
+ } else if (hasNativeSources(sharedNativeRootDir)) {
+ sourceRootDir = sharedNativeRootDir
+ }
+
+ if (sourceRootDir == null) {
+ throw new GradleException(
+ "Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " +
+ "or ../../shared-native/android/**/*.{kt,java}."
+ )
+ }
+
+ if (sourceRootDir != localSharedNativeRootDir) {
+ project.delete(localSharedNativeRootDir)
+ project.copy {
+ from(sourceRootDir)
+ into(localSharedNativeRootDir)
+ }
+ } else if (!hasNativeSources(localSharedNativeRootDir)) {
+ throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.")
+ }
+ }
+}
+
+tasks.matching { it.name == "preBuild" }.configureEach {
+ dependsOn("syncSharedShimmerSources")
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ dependsOn("syncSharedShimmerSources")
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation "com.facebook.react:react-native:+"
+}
+
+if (isNewArchitectureEnabled()) {
+ react {
+ jsRootDir = file("../src/")
+ libraryName = "StreamChatExpo"
+ codegenJavaPackageName = "com.streamchatexpo"
+ }
+}
diff --git a/package/expo-package/android/gradle.properties b/package/expo-package/android/gradle.properties
new file mode 100644
index 0000000000..ff43bf1f8d
--- /dev/null
+++ b/package/expo-package/android/gradle.properties
@@ -0,0 +1,4 @@
+StreamChatExpo_kotlinVersion=1.7.0
+StreamChatExpo_minSdkVersion=21
+StreamChatExpo_targetSdkVersion=31
+StreamChatExpo_compileSdkVersion=31
diff --git a/package/expo-package/android/src/main/AndroidManifest.xml b/package/expo-package/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..17df9bf89c
--- /dev/null
+++ b/package/expo-package/android/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java b/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java
new file mode 100644
index 0000000000..cd42f18297
--- /dev/null
+++ b/package/expo-package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java
@@ -0,0 +1,35 @@
+package com.streamchatexpo;
+
+import androidx.annotation.Nullable;
+import com.facebook.react.TurboReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.module.model.ReactModuleInfo;
+import com.facebook.react.module.model.ReactModuleInfoProvider;
+import com.facebook.react.uimanager.ViewManager;
+import com.streamchatreactnative.StreamShimmerViewManager;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StreamChatExpoPackage extends TurboReactPackage {
+ @Nullable
+ @Override
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
+ return null;
+ }
+
+ @Override
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
+ return () -> {
+ final Map moduleInfos = new HashMap<>();
+ return moduleInfos;
+ };
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.singletonList(new StreamShimmerViewManager());
+ }
+}
diff --git a/package/expo-package/package.json b/package/expo-package/package.json
index 1fce674308..4108730345 100644
--- a/package/expo-package/package.json
+++ b/package/expo-package/package.json
@@ -11,6 +11,17 @@
"url": "https://github.com/GetStream/stream-chat-react-native.git",
"directory": "package/expo-package"
},
+ "files": [
+ "src",
+ "types",
+ "android/src",
+ "android/build.gradle",
+ "android/gradle.properties",
+ "ios",
+ "*.podspec",
+ "react-native.config.js",
+ "package.json"
+ ],
"license": "SEE LICENSE IN LICENSE",
"main": "src/index.js",
"types": "types/index.d.ts",
@@ -20,6 +31,7 @@
},
"peerDependencies": {
"expo": ">=51.0.0",
+ "react-native": ">=0.76.0",
"expo-av": "*",
"expo-clipboard": "*",
"expo-document-picker": "*",
@@ -70,8 +82,19 @@
"expo-audio": "~0.4.6"
},
"scripts": {
- "prepack": " cp ../../README.md .",
- "postpack": "rm README.md"
+ "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh expo-package; fi",
+ "prepack": "bash ../scripts/sync-shared-native.sh expo-package && cp ../../README.md .",
+ "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh expo-package"
+ },
+ "codegenConfig": {
+ "name": "StreamChatExpoSpec",
+ "type": "all",
+ "jsSrcsDir": "src/native",
+ "ios": {
+ "componentProvider": {
+ "StreamShimmerView": "StreamShimmerViewComponentView"
+ }
+ }
},
"resolutions": {
"@types/react": "^19.0.0"
diff --git a/package/expo-package/react-native.config.js b/package/expo-package/react-native.config.js
new file mode 100644
index 0000000000..a94bce2790
--- /dev/null
+++ b/package/expo-package/react-native.config.js
@@ -0,0 +1,13 @@
+module.exports = {
+ dependency: {
+ platforms: {
+ android: {
+ packageImportPath: 'import com.streamchatexpo.StreamChatExpoPackage;',
+ packageInstance: 'new StreamChatExpoPackage()',
+ },
+ ios: {
+ podspecPath: 'stream-chat-expo.podspec',
+ },
+ },
+ },
+};
diff --git a/package/expo-package/src/index.js b/package/expo-package/src/index.js
index f9fbff91d6..53960194f9 100644
--- a/package/expo-package/src/index.js
+++ b/package/expo-package/src/index.js
@@ -10,6 +10,7 @@ import {
getLocalAssetUri,
getPhotos,
iOS14RefreshGallerySelection,
+ NativeShimmerView,
oniOS14GalleryLibrarySelectionChange,
overrideAudioRecordingConfiguration,
pickDocument,
@@ -31,6 +32,7 @@ registerNativeHandlers({
getLocalAssetUri,
getPhotos,
iOS14RefreshGallerySelection,
+ NativeShimmerView,
oniOS14GalleryLibrarySelectionChange,
overrideAudioRecordingConfiguration,
pickDocument,
diff --git a/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts
new file mode 100644
index 0000000000..8ba18ab123
--- /dev/null
+++ b/package/expo-package/src/native/StreamShimmerViewNativeComponent.ts
@@ -0,0 +1,14 @@
+import type { ColorValue, HostComponent, ViewProps } from 'react-native';
+
+import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
+import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
+
+export interface NativeProps extends ViewProps {
+ baseColor?: ColorValue;
+ enabled?: WithDefault;
+ gradientColor?: ColorValue;
+}
+
+export default codegenNativeComponent(
+ 'StreamShimmerView',
+) as HostComponent;
diff --git a/package/expo-package/src/optionalDependencies/NativeShimmerView.ts b/package/expo-package/src/optionalDependencies/NativeShimmerView.ts
new file mode 100644
index 0000000000..fbbe898275
--- /dev/null
+++ b/package/expo-package/src/optionalDependencies/NativeShimmerView.ts
@@ -0,0 +1,3 @@
+import StreamShimmerViewNativeComponent from '../native/StreamShimmerViewNativeComponent';
+
+export const NativeShimmerView = StreamShimmerViewNativeComponent;
diff --git a/package/expo-package/src/optionalDependencies/index.ts b/package/expo-package/src/optionalDependencies/index.ts
index 86122fb6f1..5cc6f86346 100644
--- a/package/expo-package/src/optionalDependencies/index.ts
+++ b/package/expo-package/src/optionalDependencies/index.ts
@@ -3,6 +3,7 @@ export * from './deleteFile';
export * from './getLocalAssetUri';
export * from './getPhotos';
export * from './iOS14RefreshGallerySelection';
+export * from './NativeShimmerView';
export * from './oniOS14GalleryLibrarySelectionChange';
export * from './pickDocument';
export * from './pickImage';
diff --git a/package/expo-package/stream-chat-expo.podspec b/package/expo-package/stream-chat-expo.podspec
new file mode 100644
index 0000000000..0a9f977668
--- /dev/null
+++ b/package/expo-package/stream-chat-expo.podspec
@@ -0,0 +1,25 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+
+Pod::Spec.new do |s|
+ s.name = "stream-chat-expo"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.homepage = "https://www.npmjs.com/package/stream-chat-expo"
+ s.license = package["license"]
+ s.authors = package["author"]
+
+ s.platforms = { :ios => min_ios_version_supported }
+ s.source = { :git => "./ios", :tag => "#{s.version}" }
+ s.prepare_command = <<-CMD
+ if [ -d ../shared-native/ios ]; then
+ mkdir -p ios/shared
+ rsync -a --delete ../shared-native/ios/ ios/shared/
+ fi
+ CMD
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
+ s.private_header_files = "ios/**/*.h"
+
+ install_modules_dependencies(s)
+end
diff --git a/package/native-package/.gitignore b/package/native-package/.gitignore
index 2dc3e3178e..81e0235dce 100644
--- a/package/native-package/.gitignore
+++ b/package/native-package/.gitignore
@@ -1,2 +1,7 @@
# android
android/build
+android/src/main/java/com/streamchatreactnative/shared
+
+# ios
+ios/build
+ios/shared
diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle
index 1c0d25b343..6113b5c74a 100644
--- a/package/native-package/android/build.gradle
+++ b/package/native-package/android/build.gradle
@@ -1,3 +1,8 @@
+def kotlinVersion =
+ rootProject.ext.has("kotlinVersion")
+ ? rootProject.ext.get("kotlinVersion")
+ : project.properties["ImageResizer_kotlinVersion"]
+
buildscript {
repositories {
google()
@@ -6,7 +11,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
-
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
@@ -15,6 +20,7 @@ def isNewArchitectureEnabled() {
}
apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
@@ -30,6 +36,11 @@ def getExtOrDefault(name) {
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
}
+def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
+def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
+def hasNativeSources = { File dir ->
+ dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
+}
android {
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
@@ -65,11 +76,16 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"src/newarch",
+ "src/main/java/com/streamchatreactnative/shared",
// This is needed to build Kotlin project with NewArch enabled
"${project.buildDir}/generated/source/codegen/java"
]
@@ -80,6 +96,44 @@ android {
}
}
+tasks.register("syncSharedShimmerSources") {
+ outputs.dir(localSharedNativeRootDir)
+ outputs.upToDateWhen { false }
+ doLast {
+ def sourceRootDir = null
+ if (hasNativeSources(localSharedNativeRootDir)) {
+ sourceRootDir = localSharedNativeRootDir
+ } else if (hasNativeSources(sharedNativeRootDir)) {
+ sourceRootDir = sharedNativeRootDir
+ }
+
+ if (sourceRootDir == null) {
+ throw new GradleException(
+ "Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " +
+ "or ../../shared-native/android/**/*.{kt,java}."
+ )
+ }
+
+ if (sourceRootDir != localSharedNativeRootDir) {
+ project.delete(localSharedNativeRootDir)
+ project.copy {
+ from(sourceRootDir)
+ into(localSharedNativeRootDir)
+ }
+ } else if (!hasNativeSources(localSharedNativeRootDir)) {
+ throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.")
+ }
+ }
+}
+
+tasks.matching { it.name == "preBuild" }.configureEach {
+ dependsOn("syncSharedShimmerSources")
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ dependsOn("syncSharedShimmerSources")
+}
+
repositories {
mavenCentral()
google()
diff --git a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java
index 203b72b857..9f2decab6d 100644
--- a/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java
+++ b/package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java
@@ -6,9 +6,11 @@
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
-
+import com.facebook.react.uimanager.ViewManager;
import java.util.HashMap;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
public class StreamChatReactNativePackage extends TurboReactPackage {
@@ -42,4 +44,9 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() {
return moduleInfos;
};
}
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.singletonList(new StreamShimmerViewManager());
+ }
}
diff --git a/package/native-package/package.json b/package/native-package/package.json
index a29777ba10..1dcd41ad08 100644
--- a/package/native-package/package.json
+++ b/package/native-package/package.json
@@ -15,7 +15,10 @@
"files": [
"src",
"types",
- "android",
+ "android/src",
+ "android/build.gradle",
+ "android/gradle.properties",
+ "android/gradle",
"ios",
"*.podspec",
"package.json"
@@ -78,16 +81,22 @@
}
},
"scripts": {
- "prepack": " cp ../../README.md .",
- "postpack": "rm README.md"
+ "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi",
+ "prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .",
+ "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package"
},
"devDependencies": {
"react-native": "^0.79.3"
},
"codegenConfig": {
"name": "StreamChatReactNativeSpec",
- "type": "modules",
- "jsSrcsDir": "src/native"
+ "type": "all",
+ "jsSrcsDir": "src/native",
+ "ios": {
+ "componentProvider": {
+ "StreamShimmerView": "StreamShimmerViewComponentView"
+ }
+ }
},
"resolutions": {
"@types/react": "^19.0.0"
diff --git a/package/native-package/src/index.js b/package/native-package/src/index.js
index c782abf541..ee090a1cc6 100644
--- a/package/native-package/src/index.js
+++ b/package/native-package/src/index.js
@@ -11,6 +11,7 @@ import {
getLocalAssetUri,
getPhotos,
iOS14RefreshGallerySelection,
+ NativeShimmerView,
oniOS14GalleryLibrarySelectionChange,
overrideAudioRecordingConfiguration,
pickDocument,
@@ -32,6 +33,7 @@ registerNativeHandlers({
getLocalAssetUri,
getPhotos,
iOS14RefreshGallerySelection,
+ NativeShimmerView,
oniOS14GalleryLibrarySelectionChange,
overrideAudioRecordingConfiguration,
pickDocument,
diff --git a/package/native-package/src/native/StreamShimmerViewNativeComponent.ts b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts
new file mode 100644
index 0000000000..8ba18ab123
--- /dev/null
+++ b/package/native-package/src/native/StreamShimmerViewNativeComponent.ts
@@ -0,0 +1,14 @@
+import type { ColorValue, HostComponent, ViewProps } from 'react-native';
+
+import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
+import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
+
+export interface NativeProps extends ViewProps {
+ baseColor?: ColorValue;
+ enabled?: WithDefault;
+ gradientColor?: ColorValue;
+}
+
+export default codegenNativeComponent(
+ 'StreamShimmerView',
+) as HostComponent;
diff --git a/package/native-package/src/optionalDependencies/NativeShimmerView.ts b/package/native-package/src/optionalDependencies/NativeShimmerView.ts
new file mode 100644
index 0000000000..fbbe898275
--- /dev/null
+++ b/package/native-package/src/optionalDependencies/NativeShimmerView.ts
@@ -0,0 +1,3 @@
+import StreamShimmerViewNativeComponent from '../native/StreamShimmerViewNativeComponent';
+
+export const NativeShimmerView = StreamShimmerViewNativeComponent;
diff --git a/package/native-package/src/optionalDependencies/index.ts b/package/native-package/src/optionalDependencies/index.ts
index 776ef08ba7..1b1ddee508 100644
--- a/package/native-package/src/optionalDependencies/index.ts
+++ b/package/native-package/src/optionalDependencies/index.ts
@@ -4,6 +4,7 @@ export * from './FlatList';
export * from './getLocalAssetUri';
export * from './getPhotos';
export * from './iOS14RefreshGallerySelection';
+export * from './NativeShimmerView';
export * from './oniOS14GalleryLibrarySelectionChange';
export * from './pickDocument';
export * from './pickImage';
diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec
index 32f974ca8a..d1c4a58a24 100644
--- a/package/native-package/stream-chat-react-native.podspec
+++ b/package/native-package/stream-chat-react-native.podspec
@@ -1,7 +1,6 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "stream-chat-react-native"
@@ -11,28 +10,16 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.authors = package["author"]
- s.platforms = { :ios => "10.0" }
+ s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "./ios", :tag => "#{s.version}" }
+ s.prepare_command = <<-CMD
+ if [ -d ../shared-native/ios ]; then
+ mkdir -p ios/shared
+ rsync -a --delete ../shared-native/ios/ ios/shared/
+ fi
+ CMD
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
+ s.private_header_files = "ios/**/*.h"
- s.source_files = "ios/**/*.{h,m,mm}"
-
- s.dependency "React-Core"
- s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
-
- # Don't install the dependencies when we run `pod install` in the old architecture.
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
- s.pod_target_xcconfig = {
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
- "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
- }
- s.dependency "React-Codegen"
- s.dependency "RCT-Folly"
- s.dependency "RCTRequired"
- s.dependency "RCTTypeSafety"
- s.dependency "ReactCommon/turbomodule/core"
- install_modules_dependencies(s)
- end
+ install_modules_dependencies(s)
end
-
diff --git a/package/package.json b/package/package.json
index ae2aa00e6b..ffea4c198b 100644
--- a/package/package.json
+++ b/package/package.json
@@ -24,6 +24,8 @@
],
"scripts": {
"install-all": "(yarn install --force && (cd native-package && yarn install --force) && (cd expo-package && yarn install --force))",
+ "shared-native:sync": "bash ./scripts/sync-shared-native.sh all",
+ "shared-native:clean-copies": "bash ./scripts/clean-shared-native-copies.sh all",
"build": "rimraf lib && yarn run --silent build-translations && bob build && yarn run --silent copy-translations",
"build-translations": "i18next-cli sync",
"copy-translations": "echo '\u001b[34mℹ\u001b[0m Copying translation files to \u001b[34mlib/typescript/i18n\u001b[0m' && cp -R -f ./src/i18n ./lib/typescript/i18n && echo '\u001b[32m✓\u001b[0m Done Copying Translations'",
diff --git a/package/scripts/clean-shared-native-copies.sh b/package/scripts/clean-shared-native-copies.sh
new file mode 100644
index 0000000000..f4eb986d4e
--- /dev/null
+++ b/package/scripts/clean-shared-native-copies.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+TARGET="${1:-all}"
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+
+clean_package() {
+ local package_name="$1"
+ local android_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared"
+ local ios_dir="$ROOT_DIR/$package_name/ios/shared"
+
+ rm -rf "$android_dir" "$ios_dir"
+}
+
+case "$TARGET" in
+ native-package)
+ clean_package "native-package"
+ ;;
+ expo-package)
+ clean_package "expo-package"
+ ;;
+ all)
+ clean_package "native-package"
+ clean_package "expo-package"
+ ;;
+ *)
+ echo "Unknown target: $TARGET"
+ echo "Expected one of: native-package, expo-package, all"
+ exit 1
+ ;;
+esac
+
+echo "Cleaned generated shared native copies for target: $TARGET"
diff --git a/package/scripts/reconcile-shared-native.sh b/package/scripts/reconcile-shared-native.sh
new file mode 100755
index 0000000000..d66fa5b2ed
--- /dev/null
+++ b/package/scripts/reconcile-shared-native.sh
@@ -0,0 +1,175 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+TARGET="${1:-all}"
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+STATE_DIR="$ROOT_DIR/shared-native/.sync-state"
+SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android"
+SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios"
+
+sync_dir_contents() {
+ local src_dir="$1"
+ local dst_dir="$2"
+
+ mkdir -p "$dst_dir"
+
+ if command -v rsync >/dev/null 2>&1; then
+ rsync -a --delete "$src_dir"/ "$dst_dir"/
+ else
+ find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
+ cp -R "$src_dir"/. "$dst_dir"/
+ fi
+}
+
+hash_manifest_for_dir() {
+ local dir="$1"
+ local manifest_path="$2"
+
+ mkdir -p "$(dirname "$manifest_path")"
+ : > "$manifest_path"
+
+ if [ ! -d "$dir" ]; then
+ return
+ fi
+
+ while IFS= read -r file; do
+ local rel_path="${file#$dir/}"
+ local hash
+ hash="$(shasum "$file" | awk '{print $1}')"
+ printf "%s\t%s\n" "$rel_path" "$hash" >> "$manifest_path"
+ done < <(find "$dir" -type f | LC_ALL=C sort)
+}
+
+hash_for_path() {
+ local manifest_path="$1"
+ local rel_path="$2"
+ awk -F '\t' -v path="$rel_path" '$1 == path { print $2; found=1; exit } END { if (!found) print "-" }' "$manifest_path"
+}
+
+sync_platform_with_conflict_detection() {
+ local package_name="$1"
+ local platform_name="$2"
+ local package_dir="$3"
+ local shared_dir="$4"
+
+ mkdir -p "$STATE_DIR"
+ mkdir -p "$package_dir" "$shared_dir"
+
+ local baseline_manifest="$STATE_DIR/${package_name}_${platform_name}.manifest"
+ local tmp_baseline_manifest
+ local tmp_shared_manifest
+ local tmp_package_manifest
+ local tmp_union_paths
+ local tmp_conflicts
+
+ tmp_baseline_manifest="$(mktemp)"
+ tmp_shared_manifest="$(mktemp)"
+ tmp_package_manifest="$(mktemp)"
+ tmp_union_paths="$(mktemp)"
+ tmp_conflicts="$(mktemp)"
+
+ if [ -f "$baseline_manifest" ]; then
+ cp "$baseline_manifest" "$tmp_baseline_manifest"
+ fi
+
+ hash_manifest_for_dir "$shared_dir" "$tmp_shared_manifest"
+ hash_manifest_for_dir "$package_dir" "$tmp_package_manifest"
+
+ cat "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" \
+ | awk -F '\t' 'NF > 0 { print $1 }' \
+ | LC_ALL=C sort -u > "$tmp_union_paths"
+
+ local shared_changed=0
+ local package_changed=0
+ local conflict_count=0
+
+ while IFS= read -r rel_path; do
+ [ -z "$rel_path" ] && continue
+
+ local baseline_hash
+ local shared_hash
+ local package_hash
+ baseline_hash="$(hash_for_path "$tmp_baseline_manifest" "$rel_path")"
+ shared_hash="$(hash_for_path "$tmp_shared_manifest" "$rel_path")"
+ package_hash="$(hash_for_path "$tmp_package_manifest" "$rel_path")"
+
+ if [ "$shared_hash" != "$baseline_hash" ]; then
+ shared_changed=1
+ fi
+ if [ "$package_hash" != "$baseline_hash" ]; then
+ package_changed=1
+ fi
+
+ if [ "$shared_hash" != "$package_hash" ] && [ "$shared_hash" != "$baseline_hash" ] && [ "$package_hash" != "$baseline_hash" ]; then
+ conflict_count=$((conflict_count + 1))
+ printf "%s (shared-native=%s, package=%s, baseline=%s)\n" \
+ "$rel_path" "$shared_hash" "$package_hash" "$baseline_hash" >> "$tmp_conflicts"
+ fi
+ done < "$tmp_union_paths"
+
+ if [ "$conflict_count" -gt 0 ]; then
+ echo "Conflict detected for $package_name [$platform_name]."
+ echo "Both shared-native and package mirror changed the same file(s) differently since last sync:"
+ cat "$tmp_conflicts"
+ rm -f "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" "$tmp_union_paths" "$tmp_conflicts"
+ return 1
+ fi
+
+ if [ "$package_changed" -eq 1 ] && [ "$shared_changed" -eq 0 ]; then
+ sync_dir_contents "$package_dir" "$shared_dir"
+ echo "Applied $platform_name sync direction: package -> shared-native ($package_name)"
+ elif [ "$shared_changed" -eq 1 ] && [ "$package_changed" -eq 0 ]; then
+ sync_dir_contents "$shared_dir" "$package_dir"
+ echo "Applied $platform_name sync direction: shared-native -> package ($package_name)"
+ fi
+
+ hash_manifest_for_dir "$shared_dir" "$baseline_manifest"
+ rm -f "$tmp_baseline_manifest" "$tmp_shared_manifest" "$tmp_package_manifest" "$tmp_union_paths" "$tmp_conflicts"
+}
+
+sync_from_package() {
+ local package_name="$1"
+ local android_package_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared"
+ local ios_package_dir="$ROOT_DIR/$package_name/ios/shared"
+
+ sync_platform_with_conflict_detection \
+ "$package_name" \
+ "android" \
+ "$android_package_dir" \
+ "$SHARED_ANDROID_DIR"
+
+ sync_platform_with_conflict_detection \
+ "$package_name" \
+ "ios" \
+ "$ios_package_dir" \
+ "$SHARED_IOS_DIR"
+}
+
+case "$TARGET" in
+ native-package)
+ sync_from_package "native-package"
+ ;;
+ expo-package)
+ sync_from_package "expo-package"
+ ;;
+ all)
+ # Prefer native-package when both are present.
+ if [ -d "$ROOT_DIR/native-package/android/src/main/java/com/streamchatreactnative/shared" ] || [ -d "$ROOT_DIR/native-package/ios/shared" ]; then
+ sync_from_package "native-package"
+ elif [ -d "$ROOT_DIR/expo-package/android/src/main/java/com/streamchatreactnative/shared" ] || [ -d "$ROOT_DIR/expo-package/ios/shared" ]; then
+ sync_from_package "expo-package"
+ else
+ echo "No package shared native sources found to sync from"
+ exit 1
+ fi
+ ;;
+ *)
+ echo "Unknown target: $TARGET"
+ echo "Expected one of: native-package, expo-package, all"
+ exit 1
+ ;;
+esac
+
+echo "Reconciled package/shared-native directories with conflict checks for target: $TARGET"
diff --git a/package/scripts/sync-shared-native.sh b/package/scripts/sync-shared-native.sh
new file mode 100644
index 0000000000..8d09ded9d5
--- /dev/null
+++ b/package/scripts/sync-shared-native.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+TARGET="${1:-all}"
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+SHARED_ANDROID_DIR="$ROOT_DIR/shared-native/android"
+SHARED_IOS_DIR="$ROOT_DIR/shared-native/ios"
+
+sync_dir_contents() {
+ local src_dir="$1"
+ local dst_dir="$2"
+
+ mkdir -p "$dst_dir"
+
+ if command -v rsync >/dev/null 2>&1; then
+ rsync -a --delete "$src_dir"/ "$dst_dir"/
+ else
+ find "$dst_dir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
+ cp -R "$src_dir"/. "$dst_dir"/
+ fi
+}
+
+copy_to_package() {
+ local package_name="$1"
+ local android_dst_dir="$ROOT_DIR/$package_name/android/src/main/java/com/streamchatreactnative/shared"
+ local ios_dst_dir="$ROOT_DIR/$package_name/ios/shared"
+
+ if [ -d "$SHARED_ANDROID_DIR" ]; then
+ sync_dir_contents "$SHARED_ANDROID_DIR" "$android_dst_dir"
+ else
+ echo "Skipping Android sync: missing $SHARED_ANDROID_DIR"
+ fi
+
+ if [ -d "$SHARED_IOS_DIR" ]; then
+ sync_dir_contents "$SHARED_IOS_DIR" "$ios_dst_dir"
+ else
+ echo "Skipping iOS sync: missing $SHARED_IOS_DIR"
+ fi
+}
+
+case "$TARGET" in
+ native-package)
+ copy_to_package "native-package"
+ ;;
+ expo-package)
+ copy_to_package "expo-package"
+ ;;
+ all)
+ copy_to_package "native-package"
+ copy_to_package "expo-package"
+ ;;
+ *)
+ echo "Unknown target: $TARGET"
+ echo "Expected one of: native-package, expo-package, all"
+ exit 1
+ ;;
+esac
+
+echo "Synchronized shared native directories for target: $TARGET"
diff --git a/package/shared-native/android/StreamShimmerFrameLayout.kt b/package/shared-native/android/StreamShimmerFrameLayout.kt
new file mode 100644
index 0000000000..a82dcafbbb
--- /dev/null
+++ b/package/shared-native/android/StreamShimmerFrameLayout.kt
@@ -0,0 +1,248 @@
+package com.streamchatreactnative
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.LinearGradient
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.view.View
+import android.view.animation.LinearInterpolator
+import android.widget.FrameLayout
+import kotlin.math.roundToInt
+
+/**
+ * Native shimmer container used by `StreamShimmerView`.
+ *
+ * This view draws a base color plus a moving highlight strip directly on canvas and still behaves
+ * like a regular container for React children. The animation runs fully on the native side so it
+ * does not depend on JS-driven frame updates. It automatically stops animating when the view is
+ * detached or not visible, rebuilds its shader when size or colors change, and waits for valid
+ * dimensions before starting animation to avoid invalid draw/animation states.
+ */
+class StreamShimmerFrameLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : FrameLayout(context, attrs) {
+ private var baseColor: Int = DEFAULT_BASE_COLOR
+ private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
+ private var enabled: Boolean = true
+
+ private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
+ private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.FILL
+ isDither = true
+ }
+ private val shimmerMatrix = Matrix()
+
+ private var shimmerShader: LinearGradient? = null
+ private var shimmerTranslateX: Float = 0f
+ private var animatedViewWidth: Float = 0f
+ private var animator: ValueAnimator? = null
+
+ init {
+ setWillNotDraw(false)
+ }
+
+ fun setBaseColor(color: Int) {
+ if (baseColor == color) return
+ baseColor = color
+ rebuildShimmerShader()
+ invalidate()
+ }
+
+ fun setGradientColor(color: Int) {
+ if (gradientColor == color) return
+ gradientColor = color
+ rebuildShimmerShader()
+ invalidate()
+ }
+
+ fun setShimmerEnabled(enabled: Boolean) {
+ if (this.enabled == enabled) return
+ this.enabled = enabled
+ updateAnimatorState()
+ invalidate()
+ }
+
+ fun updateAnimatorState() {
+ // Centralized lifecycle gate for animation start/stop. This keeps shimmer off for detached or
+ // hidden views to avoid wasting UI-thread work in long lists.
+ if (shouldAnimateShimmer()) {
+ startShimmer()
+ } else {
+ stopShimmer()
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ // Reattachment (including reparenting) should recheck visibility state and restart only if
+ // this instance is eligible to animate.
+ updateAnimatorState()
+ }
+
+ override fun onDetachedFromWindow() {
+ // Detached views are not drawable; stop and clear animator so a future attach starts cleanly.
+ stopShimmer()
+ super.onDetachedFromWindow()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ rebuildShimmerShader()
+ updateAnimatorState()
+ }
+
+ override fun onWindowVisibilityChanged(visibility: Int) {
+ super.onWindowVisibilityChanged(visibility)
+ updateAnimatorState()
+ }
+
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
+ super.onVisibilityChanged(changedView, visibility)
+ if (changedView === this) {
+ updateAnimatorState()
+ }
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ val viewWidth = width.toFloat()
+ val viewHeight = height.toFloat()
+ if (viewWidth <= 0f || viewHeight <= 0f) {
+ super.dispatchDraw(canvas)
+ return
+ }
+
+ basePaint.color = baseColor
+ canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint)
+
+ drawShimmer(canvas, viewWidth, viewHeight)
+ super.dispatchDraw(canvas)
+ }
+
+ private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
+ if (!enabled) return
+
+ val shader = shimmerShader ?: return
+
+ shimmerMatrix.setTranslate(shimmerTranslateX, 0f)
+ shader.setLocalMatrix(shimmerMatrix)
+ shimmerPaint.shader = shader
+ canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint)
+ shimmerPaint.shader = null
+ }
+
+ private fun rebuildShimmerShader() {
+ // Recreates the shimmer gradient for the current width/colors. This allocates shader state,
+ // so keep calls tied to real changes (size or color updates), not per frame execution.
+ val viewWidth = width.toFloat()
+ if (viewWidth <= 0f) {
+ shimmerShader = null
+ return
+ }
+
+ // Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look.
+ val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
+ val transparentHighlight = colorWithAlpha(gradientColor, 0f)
+ val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR)
+ val softBase = colorWithAlpha(gradientColor, SOFT_HIGHLIGHT_ALPHA_FACTOR)
+ val mediumBase = colorWithAlpha(gradientColor, MID_HIGHLIGHT_ALPHA_FACTOR)
+ val innerBase = colorWithAlpha(gradientColor, INNER_HIGHLIGHT_ALPHA_FACTOR)
+ shimmerShader = LinearGradient(
+ 0f,
+ 0f,
+ shimmerWidth,
+ 0f,
+ intArrayOf(
+ transparentHighlight,
+ edgeBase,
+ softBase,
+ mediumBase,
+ innerBase,
+ gradientColor,
+ innerBase,
+ mediumBase,
+ softBase,
+ edgeBase,
+ transparentHighlight,
+ ),
+ floatArrayOf(
+ 0f,
+ 0.08f,
+ 0.2f,
+ 0.32f,
+ 0.4f,
+ 0.5f,
+ 0.6f,
+ 0.68f,
+ 0.8f,
+ 0.92f,
+ 1f,
+ ),
+ Shader.TileMode.CLAMP,
+ )
+ }
+
+ private fun startShimmer() {
+ val viewWidth = width.toFloat()
+ if (viewWidth <= 0f) return
+ // Keep the existing animator if the same-sized shimmer is already active.
+ if (animator != null && animatedViewWidth == viewWidth) return
+
+ stopShimmer()
+
+ // Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
+ val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
+ animatedViewWidth = viewWidth
+ animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
+ duration = SHIMMER_DURATION_MS
+ repeatCount = ValueAnimator.INFINITE
+ interpolator = LinearInterpolator()
+ addUpdateListener {
+ shimmerTranslateX = it.animatedValue as Float
+ invalidate()
+ }
+ start()
+ }
+ }
+
+ private fun stopShimmer() {
+ animator?.cancel()
+ animator = null
+ animatedViewWidth = 0f
+ }
+
+ private fun shouldAnimateShimmer(): Boolean {
+ // `isShown` and explicit visibility/window checks cover different hide paths in nested
+ // hierarchies. Keeping them all prevents animations running when not visible to the user.
+ return enabled &&
+ isAttachedToWindow &&
+ width > 0 &&
+ height > 0 &&
+ visibility == View.VISIBLE &&
+ windowVisibility == View.VISIBLE &&
+ isShown &&
+ alpha > 0f
+ }
+
+ private fun colorWithAlpha(color: Int, alphaFactor: Float): Int {
+ // Preserve RGB while shaping only alpha; used for symmetric highlight falloff in gradient stops.
+ val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255)
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
+ }
+
+ companion object {
+ private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
+ private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
+ private const val SHIMMER_DURATION_MS = 1200L
+ private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
+ private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
+ private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f
+ private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.48f
+ private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.72f
+ }
+}
diff --git a/package/shared-native/android/StreamShimmerViewManager.kt b/package/shared-native/android/StreamShimmerViewManager.kt
new file mode 100644
index 0000000000..3110055853
--- /dev/null
+++ b/package/shared-native/android/StreamShimmerViewManager.kt
@@ -0,0 +1,81 @@
+package com.streamchatreactnative
+
+import androidx.annotation.NonNull
+import com.facebook.react.uimanager.ViewManagerDelegate
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.ViewGroupManager
+import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate
+import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface
+
+/**
+ * Fabric manager for StreamShimmerView.
+ *
+ * It creates the native shimmer layout, maps React props to native setters, and exposes child
+ * management methods so Fabric can mount and unmount children correctly inside this container.
+ * The manager rechecks animation state after prop transactions and disables shimmer when a view
+ * instance is dropped as a defensive cleanup step for recycled or unmounted views. Because the
+ * shimmer view wraps React children, this must remain a real ViewGroupManager as using a non-group
+ * manager can fail in Fabric mounting paths at runtime.
+ */
+class StreamShimmerViewManager : ViewGroupManager(),
+ StreamShimmerViewManagerInterface {
+ private val delegate = StreamShimmerViewManagerDelegate(this)
+
+ override fun getName(): String = REACT_CLASS
+
+ @NonNull
+ override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout {
+ val layout = StreamShimmerFrameLayout(reactContext)
+ layout.updateAnimatorState()
+ return layout
+ }
+
+ override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) {
+ super.onAfterUpdateTransaction(view)
+ // Prop batches can change visibility/enabled/colors together, so we re-evaluate the animator once
+ // after every transaction to keep state consistent and avoid duplicate start/stop churn.
+ view.updateAnimatorState()
+ }
+
+ override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) {
+ parent.addView(child, index)
+ }
+
+ override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View {
+ return parent.getChildAt(index)
+ }
+
+ override fun getChildCount(parent: StreamShimmerFrameLayout): Int {
+ return parent.childCount
+ }
+
+ override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) {
+ parent.removeViewAt(index)
+ }
+
+ override fun getDelegate(): ViewManagerDelegate = delegate
+
+ override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) {
+ view.setShimmerEnabled(enabled)
+ }
+
+ override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) {
+ view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
+ }
+
+ override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
+ view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
+ }
+
+ override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) {
+ super.onDropViewInstance(view)
+ // Defensive shutdown for recycled/unmounted views; avoids animator leaks in list-heavy screens.
+ view.setShimmerEnabled(false)
+ }
+
+ companion object {
+ const val REACT_CLASS = "StreamShimmerView"
+ private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
+ private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
+ }
+}
diff --git a/package/shared-native/ios/StreamShimmerView.swift b/package/shared-native/ios/StreamShimmerView.swift
new file mode 100644
index 0000000000..f77946d9a9
--- /dev/null
+++ b/package/shared-native/ios/StreamShimmerView.swift
@@ -0,0 +1,235 @@
+import QuartzCore
+import UIKit
+
+/// Native shimmer view used by the Fabric component view.
+///
+/// It renders a base layer and a moving gradient highlight entirely in native code, so shimmer
+/// animation stays off the JS thread. The view updates its gradient when size or colors change and
+/// stops animation when it is not drawable (backgrounded, detached, hidden, or zero sized).
+@objcMembers
+public final class StreamShimmerView: UIView {
+ private static let edgeHighlightAlpha: CGFloat = 0.1
+ private static let softHighlightAlpha: CGFloat = 0.24
+ private static let midHighlightAlpha: CGFloat = 0.48
+ private static let innerHighlightAlpha: CGFloat = 0.72
+ private static let defaultHighlightAlpha: CGFloat = 0.35
+ private static let shimmerStripWidthRatio: CGFloat = 1.25
+ private static let shimmerDuration: CFTimeInterval = 1.2
+ private static let shimmerAnimationKey = "stream_shimmer_translate_x"
+
+ private let baseLayer = CALayer()
+ private let shimmerLayer = CAGradientLayer()
+
+ private var baseColor: UIColor = UIColor(white: 1, alpha: 0)
+ private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha)
+ private var enabled = false
+ private var lastAnimatedSize: CGSize = .zero
+ private var isAppActive = true
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupLayers()
+ setupLifecycleObservers()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupLayers()
+ setupLifecycleObservers()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ public override func layoutSubviews() {
+ super.layoutSubviews()
+ updateLayersForCurrentState()
+ }
+
+ public override func didMoveToWindow() {
+ super.didMoveToWindow()
+ if window == nil {
+ // Detaching from window means this view is no longer drawable. Stop and clear animation so
+ // a later reattach starts from a clean state.
+ stopAnimation()
+ } else {
+ // Reattaching (including reparenting across windows) re-evaluates state and restarts only
+ // when needed by current bounds/visibility/enablement.
+ updateLayersForCurrentState()
+ }
+ }
+
+ public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+ if let previousTraitCollection,
+ traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
+ {
+ // In current usage, colors are typically driven by JS props. We still refresh on trait
+ // changes so dynamically resolved native colors remain correct if that path is used later.
+ updateLayersForCurrentState()
+ }
+ }
+
+ public func apply(
+ baseColor: UIColor,
+ gradientColor: UIColor,
+ enabled: Bool
+ ) {
+ self.baseColor = baseColor
+ self.gradientColor = gradientColor
+ self.enabled = enabled
+ updateLayersForCurrentState()
+ }
+
+ public func stopAnimation() {
+ shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey)
+ lastAnimatedSize = .zero
+ }
+
+ private func setupLayers() {
+ isUserInteractionEnabled = false
+
+ shimmerLayer.contentsScale = UIScreen.main.scale
+ shimmerLayer.allowsEdgeAntialiasing = true
+ shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
+ shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
+ shimmerLayer.locations = [0.0, 0.08, 0.2, 0.32, 0.4, 0.5, 0.6, 0.68, 0.8, 0.92, 1.0]
+
+ layer.addSublayer(baseLayer)
+ layer.addSublayer(shimmerLayer)
+ }
+
+ private func setupLifecycleObservers() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleWillEnterForeground),
+ name: UIApplication.willEnterForegroundNotification,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleDidEnterBackground),
+ name: UIApplication.didEnterBackgroundNotification,
+ object: nil
+ )
+ }
+
+ @objc
+ private func handleWillEnterForeground() {
+ // iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
+ // a state update on foreground so shimmer reliably restarts when returning to the app.
+ isAppActive = true
+ updateLayersForCurrentState()
+ }
+
+ @objc
+ private func handleDidEnterBackground() {
+ isAppActive = false
+ stopAnimation()
+ }
+
+ private func updateLayersForCurrentState() {
+ let bounds = self.bounds
+ guard !bounds.isEmpty else {
+ stopAnimation()
+ return
+ }
+
+ baseLayer.frame = bounds
+ baseLayer.backgroundColor = baseColor.cgColor
+
+ updateShimmerLayer(for: bounds)
+ updateShimmerAnimation(for: bounds)
+ }
+
+ private func updateShimmerLayer(for bounds: CGRect) {
+ // Rebuild the shimmer gradient for current width/colors. Keep this tied to real state changes
+ // such as layout/prop updates, not continuous per frame calls.
+ let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
+ let transparentHighlight = color(gradientColor, alphaFactor: 0)
+ shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height)
+ shimmerLayer.colors = [
+ transparentHighlight.cgColor,
+ color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
+ gradientColor.cgColor,
+ color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
+ color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
+ transparentHighlight.cgColor,
+ ]
+ shimmerLayer.isHidden = !enabled
+ }
+
+ private func updateShimmerAnimation(for bounds: CGRect) {
+ guard enabled, isAppActive, window != nil, bounds.width > 0, bounds.height > 0 else {
+ stopAnimation()
+ return
+ }
+
+ // If an animation already exists for the same size, keep it running instead of restarting.
+ if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil, lastAnimatedSize == bounds.size {
+ return
+ }
+
+ stopAnimation()
+
+ // Start just outside the left edge and sweep fully past the right edge for a clean pass.
+ let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
+ let animation = CABasicAnimation(keyPath: "transform.translation.x")
+ animation.fromValue = 0
+ animation.toValue = bounds.width + shimmerWidth
+ animation.duration = Self.shimmerDuration
+ animation.repeatCount = .infinity
+ animation.timingFunction = CAMediaTimingFunction(name: .linear)
+ animation.isRemovedOnCompletion = true
+ shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey)
+ lastAnimatedSize = bounds.size
+ }
+
+ private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
+ // Preserve the resolved color channels and shape only alpha for smooth highlight falloff.
+ let resolvedColor = color.resolvedColor(with: traitCollection)
+
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ if resolvedColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
+ return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor)
+ }
+
+ guard
+ let converted = resolvedColor.cgColor.converted(
+ to: CGColorSpace(name: CGColorSpace.extendedSRGB)!,
+ intent: .defaultIntent,
+ options: nil
+ ),
+ let components = converted.components
+ else {
+ return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
+ }
+
+ switch components.count {
+ case 2:
+ return UIColor(
+ white: components[0],
+ alpha: components[1] * alphaFactor
+ )
+ case 4:
+ return UIColor(
+ red: components[0],
+ green: components[1],
+ blue: components[2],
+ alpha: components[3] * alphaFactor
+ )
+ default:
+ return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
+ }
+ }
+}
diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.h b/package/shared-native/ios/StreamShimmerViewComponentView.h
new file mode 100644
index 0000000000..5fd2863c5c
--- /dev/null
+++ b/package/shared-native/ios/StreamShimmerViewComponentView.h
@@ -0,0 +1,22 @@
+#import
+
+#ifdef RCT_NEW_ARCH_ENABLED
+#import
+#endif
+
+#ifndef StreamShimmerViewComponentView_h
+#define StreamShimmerViewComponentView_h
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface StreamShimmerViewComponentView :
+#ifdef RCT_NEW_ARCH_ENABLED
+ RCTViewComponentView
+#else
+ UIView
+#endif
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif /* StreamShimmerViewComponentView_h */
diff --git a/package/shared-native/ios/StreamShimmerViewComponentView.mm b/package/shared-native/ios/StreamShimmerViewComponentView.mm
new file mode 100644
index 0000000000..c6161d18a8
--- /dev/null
+++ b/package/shared-native/ios/StreamShimmerViewComponentView.mm
@@ -0,0 +1,107 @@
+#import "StreamShimmerViewComponentView.h"
+
+#ifdef RCT_NEW_ARCH_ENABLED
+
+#if __has_include()
+#import
+#import
+#import
+#elif __has_include()
+#import
+#import
+#import
+#else
+#error "Unable to find generated codegen headers for StreamShimmerView."
+#endif
+
+#if __has_include()
+#import
+#elif __has_include()
+#import
+#elif __has_include("stream_chat_react_native-Swift.h")
+#import "stream_chat_react_native-Swift.h"
+#elif __has_include("stream_chat_expo-Swift.h")
+#import "stream_chat_expo-Swift.h"
+#else
+#error "Unable to import generated Swift header for StreamShimmerView."
+#endif
+
+#import
+
+using namespace facebook::react;
+
+@interface StreamShimmerViewComponentView ()
+@end
+
+// Fabric bridge for StreamShimmerView. This component view owns the native shimmer instance,
+// applies codegen props, and keeps shimmer rendered as a background layer while Fabric manages
+// React children. Keeping shimmer as a layer avoids child-order conflicts during mount/unmount.
+@implementation StreamShimmerViewComponentView {
+ StreamShimmerView *_shimmerView;
+}
+
++ (ComponentDescriptorProvider)componentDescriptorProvider
+{
+ return concreteComponentDescriptorProvider();
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ if (self = [super initWithFrame:frame]) {
+ static const auto defaultProps = std::make_shared();
+ _props = defaultProps;
+
+ _shimmerView = [[StreamShimmerView alloc] initWithFrame:self.bounds];
+ _shimmerView.userInteractionEnabled = NO;
+ [self.layer insertSublayer:_shimmerView.layer atIndex:0];
+ }
+
+ return self;
+}
+
+- (void)layoutSubviews
+{
+ [super layoutSubviews];
+ _shimmerView.frame = self.bounds;
+
+ // Keep shimmer pinned as the layer furthest back. Some layer operations can reorder sublayers, and
+ // this guard restores expected layering without touching Fabric managed child views.
+ BOOL needsReinsert = _shimmerView.layer.superlayer != self.layer;
+ if (!needsReinsert) {
+ CALayer *firstLayer = self.layer.sublayers.firstObject;
+ needsReinsert = firstLayer != _shimmerView.layer;
+ }
+ if (needsReinsert) {
+ [self.layer insertSublayer:_shimmerView.layer atIndex:0];
+ }
+}
+
+- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
+{
+ const auto &newProps = *std::static_pointer_cast(props);
+
+ UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor colorWithWhite:1 alpha:0];
+ UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor];
+
+ [_shimmerView applyWithBaseColor:baseColor
+ gradientColor:gradientColor
+ enabled:newProps.enabled];
+
+ [super updateProps:props oldProps:oldProps];
+}
+
+- (void)prepareForRecycle
+{
+ [super prepareForRecycle];
+ // Defensive cleanup for recycled cells/views so offscreen instances do not keep animating.
+ [_shimmerView stopAnimation];
+}
+
+- (void)dealloc
+{
+ [_shimmerView stopAnimation];
+}
+
+@end
+
+#endif
diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx
index 41c4731020..e5c1b75e30 100644
--- a/package/src/components/Attachment/Gallery.tsx
+++ b/package/src/components/Attachment/Gallery.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { ImageErrorEvent, Pressable, StyleSheet, Text, View } from 'react-native';
import type { Attachment, LocalMessage } from 'stream-chat';
@@ -35,6 +35,7 @@ import {
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useLoadingImage } from '../../hooks/useLoadingImage';
+import { useStableCallback } from '../../hooks/useStableCallback';
import { isVideoPlayerAvailable } from '../../native';
import { primitives } from '../../theme';
import { FileTypes } from '../../types/types';
@@ -59,7 +60,6 @@ export type GalleryPropsWithContext = Pick &
Pick & {
@@ -74,7 +74,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
images,
message,
onLongPress,
@@ -148,8 +147,8 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
images.length !== 1
? { width: gridWidth, height: gridHeight }
: {
- height,
- width,
+ minHeight: height,
+ minWidth: width,
},
galleryContainer,
]}
@@ -194,7 +193,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
imageGalleryStateStore={imageGalleryStateStore}
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
ImageLoadingIndicator={ImageLoadingIndicator}
- ImageReloadIndicator={ImageReloadIndicator}
imagesAndVideos={imagesAndVideos}
invertedDirections={invertedDirections || false}
key={rowIndex}
@@ -235,7 +233,6 @@ type GalleryThumbnailProps = {
| 'VideoThumbnail'
| 'ImageLoadingIndicator'
| 'ImageLoadingFailedIndicator'
- | 'ImageReloadIndicator'
> &
Pick &
Pick &
@@ -248,7 +245,6 @@ const GalleryThumbnail = ({
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
imagesAndVideos,
invertedDirections,
message,
@@ -352,7 +348,6 @@ const GalleryThumbnail = ({
borderRadius={imageBorderRadius ?? borderRadius}
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
ImageLoadingIndicator={ImageLoadingIndicator}
- ImageReloadIndicator={ImageReloadIndicator}
thumbnail={thumbnail}
/>
)}
@@ -379,15 +374,10 @@ const GalleryImageThumbnail = ({
borderRadius,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
thumbnail,
}: Pick<
GalleryThumbnailProps,
- | 'ImageLoadingFailedIndicator'
- | 'ImageLoadingIndicator'
- | 'ImageReloadIndicator'
- | 'thumbnail'
- | 'borderRadius'
+ 'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius'
>) => {
const {
isLoadingImage,
@@ -405,35 +395,39 @@ const GalleryImageThumbnail = ({
const styles = useStyles();
+ const onLoadStart = useStableCallback(() => {
+ setLoadingImageError(false);
+ setLoadingImage(true);
+ });
+
+ const onLoad = useStableCallback(() => {
+ setTimeout(() => {
+ setLoadingImage(false);
+ setLoadingImageError(false);
+ }, 0);
+ });
+
+ const onError = useStableCallback(({ nativeEvent: { error } }: ImageErrorEvent) => {
+ console.warn(error);
+ setLoadingImage(false);
+ setLoadingImageError(true);
+ });
+
return (
-
+
{isLoadingImageError ? (
- <>
-
-
- >
+
) : (
<>
{
- console.warn(error);
- setLoadingImage(false);
- setLoadingImageError(true);
- }}
- onLoadEnd={() => setTimeout(() => setLoadingImage(false), 0)}
- onLoadStart={() => setLoadingImage(true)}
+ onError={onError}
+ onLoad={onLoad}
+ onLoadStart={onLoadStart}
resizeMode={thumbnail.resizeMode}
- style={[borderRadius, gallery.image]}
+ style={gallery.image}
uri={thumbnail.url}
/>
- {isLoadingImage && (
-
-
-
- )}
+ {isLoadingImage ? : null}
>
)}
@@ -512,7 +506,6 @@ export const Gallery = (props: GalleryProps) => {
additionalPressableProps: propAdditionalPressableProps,
ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator,
ImageLoadingIndicator: PropImageLoadingIndicator,
- ImageReloadIndicator: PropImageReloadIndicator,
images: propImages,
message: propMessage,
myMessageTheme: propMyMessageTheme,
@@ -542,7 +535,6 @@ export const Gallery = (props: GalleryProps) => {
additionalPressableProps: contextAdditionalPressableProps,
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
ImageLoadingIndicator: ContextImageLoadingIndicator,
- ImageReloadIndicator: ContextImageReloadIndicator,
myMessageTheme: contextMyMessageTheme,
VideoThumbnail: ContextVideoThumnbnail,
} = useMessagesContext();
@@ -567,7 +559,6 @@ export const Gallery = (props: GalleryProps) => {
const ImageLoadingFailedIndicator =
PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator;
const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator;
- const ImageReloadIndicator = PropImageReloadIndicator || ContextImageReloadIndicator;
const myMessageTheme = propMyMessageTheme || contextMyMessageTheme;
const messageContentOrder = propMessageContentOrder || contextMessageContentOrder;
@@ -585,7 +576,6 @@ export const Gallery = (props: GalleryProps) => {
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
images,
message,
myMessageTheme,
@@ -607,6 +597,7 @@ const useStyles = () => {
const {
theme: { semantics },
} = useTheme();
+ const { isMyMessage } = useMessageContext();
return useMemo(() => {
return StyleSheet.create({
errorTextSize: {
@@ -626,11 +617,15 @@ const useStyles = () => {
imageContainer: {},
image: {
flex: 1,
+ backgroundColor: isMyMessage
+ ? semantics.chatBgAttachmentOutgoing
+ : semantics.chatBgAttachmentIncoming,
+ overflow: 'hidden',
},
imageLoadingErrorIndicatorStyle: {
- bottom: 4,
- left: 4,
- position: 'absolute',
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
},
imageLoadingIndicatorContainer: {
height: '100%',
@@ -658,8 +653,17 @@ const useStyles = () => {
lineHeight: primitives.typographyLineHeightRelaxed,
fontWeight: primitives.typographyFontWeightSemiBold,
},
+ imageLoadingErrorContainer: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ imageLoadingErrorWrapper: {
+ ...StyleSheet.absoluteFillObject,
+ overflow: 'hidden',
+ },
});
- }, [semantics]);
+ }, [semantics, isMyMessage]);
};
Gallery.displayName = 'Gallery{messageSimple{gallery}}';
diff --git a/package/src/components/Attachment/Giphy/GiphyImage.tsx b/package/src/components/Attachment/Giphy/GiphyImage.tsx
index d3f4dc551a..f9c016283f 100644
--- a/package/src/components/Attachment/Giphy/GiphyImage.tsx
+++ b/package/src/components/Attachment/Giphy/GiphyImage.tsx
@@ -39,8 +39,13 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => {
const { giphy: giphyData, image_url, thumb_url, type } = attachment;
- const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } =
- useLoadingImage();
+ const {
+ isLoadingImage,
+ isLoadingImageError,
+ setLoadingImage,
+ setLoadingImageError,
+ onReloadImage,
+ } = useLoadingImage();
const {
theme: {
@@ -93,7 +98,7 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => {
/>
{isLoadingImageError && (
-
+
)}
{isLoadingImage && (
diff --git a/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx b/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx
index 2f135835e8..aef29c352d 100644
--- a/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx
+++ b/package/src/components/Attachment/ImageLoadingFailedIndicator.tsx
@@ -1,56 +1,45 @@
-import React from 'react';
-import { StyleSheet, Text, View, ViewProps } from 'react-native';
+import React, { useMemo } from 'react';
+import { Pressable, StyleSheet, ViewProps } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
-
-import { Warning } from '../../icons/Warning';
-
-const WARNING_ICON_SIZE = 14;
+import { RetryBadge } from '../ui/Badge/RetryBadge';
+
+export type ImageLoadingFailedIndicatorProps = ViewProps & {
+ /**
+ * Callback to reload the image
+ * @returns Callback to reload the image
+ */
+ onReloadImage: () => void;
+};
-const styles = StyleSheet.create({
- container: {
- alignContent: 'center',
- alignItems: 'center',
- borderRadius: 20,
- flexDirection: 'row',
- justifyContent: 'center',
- },
- errorText: {
- fontSize: 8,
- justifyContent: 'center',
- paddingHorizontal: 8,
- },
- warningIconStyle: {
- borderRadius: 24,
- marginLeft: 4,
- marginTop: 6,
- },
-});
+export const ImageLoadingFailedIndicator = ({
+ onReloadImage,
+}: ImageLoadingFailedIndicatorProps) => {
+ const styles = useStyles();
-export type ImageLoadingFailedIndicatorProps = ViewProps;
+ return (
+
+
+
+ );
+};
-export const ImageLoadingFailedIndicator = (props: ImageLoadingFailedIndicatorProps) => {
+const useStyles = () => {
const {
- theme: {
- colors: { accent_red, overlay, white },
- },
+ theme: { semantics },
} = useTheme();
-
- const { t } = useTranslationContext();
-
- const { style, ...rest } = props;
- return (
-
-
-
- {t('Error loading')}
-
-
- );
+ return useMemo(() => {
+ return StyleSheet.create({
+ imageLoadingErrorIndicatorStyle: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: semantics.backgroundCoreOverlayLight,
+ },
+ });
+ }, [semantics]);
};
diff --git a/package/src/components/Attachment/ImageLoadingIndicator.tsx b/package/src/components/Attachment/ImageLoadingIndicator.tsx
index 019aa420d4..7bb2534f1d 100644
--- a/package/src/components/Attachment/ImageLoadingIndicator.tsx
+++ b/package/src/components/Attachment/ImageLoadingIndicator.tsx
@@ -1,31 +1,32 @@
import React from 'react';
-import { ActivityIndicator, StyleSheet, View, ViewProps } from 'react-native';
+import { ActivityIndicator, StyleSheet, View } from 'react-native';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useTheme } from '../../contexts';
+import { NativeShimmerView } from '../UIComponents/NativeShimmerView';
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- display: 'flex',
- justifyContent: 'center',
- width: '100%',
- },
-});
-
-export type ImageLoadingIndicatorProps = ViewProps;
-
-export const ImageLoadingIndicator = (props: ImageLoadingIndicatorProps) => {
+export const ImageLoadingIndicator = () => {
const {
- theme: {
- messageSimple: {
- loadingIndicator: { container },
- },
- },
+ theme: { semantics },
} = useTheme();
- const { style, ...rest } = props;
return (
-
-
-
+
+
+
+
+
);
};
+
+const styles = StyleSheet.create({
+ centered: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+});
diff --git a/package/src/components/Attachment/ImageReloadIndicator.tsx b/package/src/components/Attachment/ImageReloadIndicator.tsx
deleted file mode 100644
index 9f3b2e24a0..0000000000
--- a/package/src/components/Attachment/ImageReloadIndicator.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { Pressable } from 'react-native';
-
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Refresh } from '../../icons';
-
-const REFRESH_ICON_SIZE = 24;
-
-export type ImageReloadIndicatorProps = {
- onReloadImage: () => void;
- style: React.ComponentProps['style'];
-};
-
-export const ImageReloadIndicator = ({ onReloadImage, style }: ImageReloadIndicatorProps) => {
- const {
- theme: {
- colors: { grey_dark },
- },
- } = useTheme();
-
- return (
-
-
-
- );
-};
diff --git a/package/src/components/Attachment/__tests__/Gallery.test.js b/package/src/components/Attachment/__tests__/Gallery.test.js
index 57a06a0f3c..23a2ccdade 100644
--- a/package/src/components/Attachment/__tests__/Gallery.test.js
+++ b/package/src/components/Attachment/__tests__/Gallery.test.js
@@ -1,12 +1,6 @@
import React from 'react';
-import {
- fireEvent,
- render,
- screen,
- waitFor,
- waitForElementToBeRemoved,
-} from '@testing-library/react-native';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
@@ -26,6 +20,15 @@ import { Chat } from '../../Chat/Chat';
import { MessageList } from '../../MessageList/MessageList';
describe('Gallery', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
const user1 = generateUser();
const getComponent = async (attachments = []) => {
@@ -272,7 +275,7 @@ describe('Gallery', () => {
fireEvent(screen.getByLabelText('Gallery Image'), 'error', {
nativeEvent: { error: 'error loading image' },
});
- expect(screen.getByAccessibilityHint('image-loading-error')).toBeTruthy();
+ expect(screen.getByLabelText('Image Loading Error Indicator')).toBeTruthy();
});
it('should render a loading indicator and when successful render the image', async () => {
@@ -288,11 +291,15 @@ describe('Gallery', () => {
expect(screen.queryAllByTestId('gallery-container').length).toBe(1);
});
- fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadStart');
- expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy();
+ fireEvent(screen.getByLabelText('Gallery Image'), 'loadStart');
+ await waitFor(() => {
+ expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy();
+ });
- fireEvent(screen.getByLabelText('Gallery Image'), 'onLoadFinish');
- waitForElementToBeRemoved(() => screen.getByAccessibilityHint('image-loading'));
+ fireEvent(screen.getByLabelText('Gallery Image'), 'onLoad');
+ await waitFor(() => {
+ expect(screen.queryByLabelText('Image Loading Indicator')).toBeNull();
+ });
expect(screen.getByLabelText('Gallery Image')).toBeTruthy();
- });
+ }, 20000);
});
diff --git a/package/src/components/Attachment/__tests__/Giphy.test.js b/package/src/components/Attachment/__tests__/Giphy.test.js
index 2aed7e6385..94d3085926 100644
--- a/package/src/components/Attachment/__tests__/Giphy.test.js
+++ b/package/src/components/Attachment/__tests__/Giphy.test.js
@@ -8,7 +8,6 @@ import {
screen,
userEvent,
waitFor,
- waitForElementToBeRemoved,
} from '@testing-library/react-native';
import { MessageProvider } from '../../../contexts/messageContext/MessageContext';
@@ -321,7 +320,7 @@ describe('Giphy', () => {
});
await waitFor(() => {
- expect(screen.getByAccessibilityHint('image-loading-error')).toBeTruthy();
+ expect(screen.getByLabelText('Image Loading Error Indicator')).toBeTruthy();
});
});
@@ -335,23 +334,21 @@ describe('Giphy', () => {
,
);
- await waitFor(() => {
- expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy();
- });
-
act(() => {
- fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'onLoadStart');
+ fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'loadStart');
});
await waitFor(() => {
- expect(screen.getByAccessibilityHint('image-loading')).toBeTruthy();
+ expect(screen.getByLabelText('Image Loading Indicator')).toBeTruthy();
});
act(() => {
- fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'onLoad');
+ fireEvent(screen.getByLabelText('Giphy Attachment Image'), 'loadEnd');
});
- waitForElementToBeRemoved(() => screen.getByAccessibilityHint('image-loading'));
+ await waitFor(() => {
+ expect(screen.queryByLabelText('Image Loading Indicator')).toBeNull();
+ });
await waitFor(() => {
expect(screen.getByLabelText('Giphy Attachment Image')).toBeTruthy();
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 45091a78aa..03a2c3a0b9 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -119,7 +119,6 @@ import { Gallery as GalleryDefault } from '../Attachment/Gallery';
import { Giphy as GiphyDefault } from '../Attachment/Giphy';
import { ImageLoadingFailedIndicator as ImageLoadingFailedIndicatorDefault } from '../Attachment/ImageLoadingFailedIndicator';
import { ImageLoadingIndicator as ImageLoadingIndicatorDefault } from '../Attachment/ImageLoadingIndicator';
-import { ImageReloadIndicator as ImageReloadIndicatorDefault } from '../Attachment/ImageReloadIndicator';
import { URLPreview as URLPreviewDefault } from '../Attachment/UrlPreview';
import { VideoThumbnail as VideoThumbnailDefault } from '../Attachment/VideoThumbnail';
import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker';
@@ -351,7 +350,6 @@ export type ChannelPropsWithContext = Pick &
| 'isAttachmentEqual'
| 'ImageLoadingFailedIndicator'
| 'ImageLoadingIndicator'
- | 'ImageReloadIndicator'
| 'markdownRules'
| 'Message'
| 'MessageActionList'
@@ -647,7 +645,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
ImageAttachmentUploadPreview = ImageAttachmentUploadPreviewDefault,
ImageLoadingFailedIndicator = ImageLoadingFailedIndicatorDefault,
ImageLoadingIndicator = ImageLoadingIndicatorDefault,
- ImageReloadIndicator = ImageReloadIndicatorDefault,
initialScrollToFirstUnreadMessage = false,
InlineDateSeparator = InlineDateSeparatorDefault,
InlineUnreadIndicator = InlineUnreadIndicatorDefault,
@@ -1932,7 +1929,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
hasCreatePoll === undefined ? pollCreationEnabled : hasCreatePoll && pollCreationEnabled,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
initialScrollToFirstUnreadMessage: !messageId && initialScrollToFirstUnreadMessage, // when messageId is set, we scroll to the messageId instead of first unread
InlineDateSeparator,
InlineUnreadIndicator,
diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
index 3db83d4d65..178ce6a077 100644
--- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts
+++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
@@ -42,7 +42,6 @@ export const useCreateMessagesContext = ({
hasCreatePoll,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
initialScrollToFirstUnreadMessage,
InlineDateSeparator,
InlineUnreadIndicator,
@@ -159,7 +158,6 @@ export const useCreateMessagesContext = ({
hasCreatePoll,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
- ImageReloadIndicator,
initialScrollToFirstUnreadMessage,
InlineDateSeparator,
InlineUnreadIndicator,
diff --git a/package/src/components/ChannelList/ChannelList.tsx b/package/src/components/ChannelList/ChannelList.tsx
index e9570aafd0..f2a91b0cfb 100644
--- a/package/src/components/ChannelList/ChannelList.tsx
+++ b/package/src/components/ChannelList/ChannelList.tsx
@@ -28,7 +28,7 @@ import {
import { useChatContext } from '../../contexts/chatContext/ChatContext';
import { SwipeRegistryProvider } from '../../contexts/swipeableContext/SwipeRegistryContext';
import type { ChannelListEventListenerOptions } from '../../types/types';
-import { ChannelPreview } from '../ChannelPreview/ChannelPreview';
+import { ChannelPreviewMessenger } from '../ChannelPreview/ChannelPreviewMessenger';
import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator';
import { LoadingErrorIndicator as LoadingErrorIndicatorDefault } from '../Indicators/LoadingErrorIndicator';
@@ -279,7 +279,7 @@ export const ChannelList = (props: ChannelListProps) => {
onRemovedFromChannel,
onSelect,
options = DEFAULT_OPTIONS,
- Preview = ChannelPreview,
+ Preview = ChannelPreviewMessenger,
getChannelActionItems,
PreviewAvatar,
PreviewMessage,
diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx
index caf378e4cf..cbd6fa1569 100644
--- a/package/src/components/Message/MessageSimple/MessageContent.tsx
+++ b/package/src/components/Message/MessageSimple/MessageContent.tsx
@@ -169,7 +169,6 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
messageGroupedSingleOrBottomContainer,
messageGroupedTopContainer,
replyContainer,
- textWrapper,
wrapper,
},
},
@@ -363,11 +362,9 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
}
})}
-
- {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : (
-
- )}
-
+ {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : (
+
+ )}
@@ -665,7 +662,5 @@ const styles = StyleSheet.create({
rightAlignItems: {
alignItems: 'flex-end',
},
- textWrapper: {
- paddingHorizontal: primitives.spacingSm,
- },
+ textWrapper: {},
});
diff --git a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
index e3af2215f1..9af8b5747d 100644
--- a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
+++ b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx
@@ -17,9 +17,10 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import type { MarkdownStyle, Theme } from '../../../contexts/themeContext/utils/theme';
import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
+import { primitives } from '../../../theme';
const styles = StyleSheet.create({
- textContainer: { maxWidth: 256 },
+ textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingSm },
});
export type MessageTextProps = MessageTextContainerProps & {
diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
index 95f08ee3e1..451f0b4af2 100644
--- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
+++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap
@@ -6,6 +6,7 @@ exports[`MessageTextContainer should render message text container 1`] = `
[
{
"maxWidth": 256,
+ "paddingHorizontal": 12,
},
{},
undefined,
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx
index 919ee1c1aa..7ea9b848fe 100644
--- a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx
+++ b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx
@@ -5,9 +5,9 @@ import { LocalAttachmentUploadMetadata } from 'stream-chat';
import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { ExclamationCircle } from '../../../../icons/ExclamationCircle';
-import { RotateCircle } from '../../../../icons/RotateCircle';
import { Warning } from '../../../../icons/Warning';
import { primitives } from '../../../../theme';
+import { RetryBadge } from '../../../ui/Badge/RetryBadge';
export const FileUploadInProgressIndicator = () => {
const {
@@ -128,20 +128,9 @@ export type ImageUploadRetryIndicatorProps = {
};
export const ImageUploadRetryIndicator = ({ onRetryHandler }: ImageUploadRetryIndicatorProps) => {
- const styles = useImageUploadRetryIndicatorStyles();
- const {
- theme: {
- semantics,
- messageInput: { imageUploadRetryIndicator },
- },
- } = useTheme();
return (
-
-
+
+
);
};
@@ -174,25 +163,6 @@ const useImageUploadInProgressIndicatorStyles = () => {
});
};
-const useImageUploadRetryIndicatorStyles = () => {
- const {
- theme: { semantics },
- } = useTheme();
- return StyleSheet.create({
- container: {
- backgroundColor: semantics.accentError,
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: primitives.radiusMax,
- borderWidth: 2,
- borderColor: semantics.textOnAccent,
- alignSelf: 'center',
- width: 32,
- height: 32,
- },
- });
-};
-
const useImageUploadNotSupportedIndicatorStyles = () => {
const {
theme: { semantics },
diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
index be650c07d5..debfc46e97 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
@@ -559,61 +559,51 @@ exports[`Thread should match thread snapshot 1`] = `
style={
[
{
+ "maxWidth": 256,
"paddingHorizontal": 12,
},
{},
+ undefined,
]
}
+ testID="message-text-container"
>
-
-
- Message6
-
+ Message6
-
+
@@ -899,61 +889,51 @@ exports[`Thread should match thread snapshot 1`] = `
style={
[
{
+ "maxWidth": 256,
"paddingHorizontal": 12,
},
{},
+ undefined,
]
}
+ testID="message-text-container"
>
-
-
- Message5
-
+ Message5
-
+
@@ -1272,61 +1252,51 @@ exports[`Thread should match thread snapshot 1`] = `
style={
[
{
+ "maxWidth": 256,
"paddingHorizontal": 12,
},
{},
+ undefined,
]
}
+ testID="message-text-container"
>
-
-
- Message4
-
+ Message4
-
+
@@ -1603,61 +1573,51 @@ exports[`Thread should match thread snapshot 1`] = `
style={
[
{
+ "maxWidth": 256,
"paddingHorizontal": 12,
},
{},
+ undefined,
]
}
+ testID="message-text-container"
>
-
-
- Message3
-
+ Message3
-
+
diff --git a/package/src/components/UIComponents/NativeShimmerView.tsx b/package/src/components/UIComponents/NativeShimmerView.tsx
new file mode 100644
index 0000000000..ea3d56431e
--- /dev/null
+++ b/package/src/components/UIComponents/NativeShimmerView.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { ColorValue, View, ViewProps } from 'react-native';
+
+import { NativeHandlers } from '../../native';
+
+export type NativeShimmerViewProps = ViewProps & {
+ baseColor?: ColorValue;
+ enabled?: boolean;
+ gradientColor?: ColorValue;
+};
+
+export const NativeShimmerView = (props: NativeShimmerViewProps) => {
+ const NativeShimmerComponent = NativeHandlers.NativeShimmerView;
+ if (NativeShimmerComponent) {
+ return ;
+ }
+
+ const { children, ...rest } = props;
+ return {children};
+};
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index 9c0c493935..b5402f95a1 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -7,7 +7,6 @@ export * from './Attachment/FileIcon';
export * from './Attachment/Gallery';
export * from './Attachment/Giphy';
export * from './Attachment/VideoThumbnail';
-export * from './Attachment/ImageReloadIndicator';
export * from './Attachment/UrlPreview';
export * from './Attachment/utils/buildGallery/buildGallery';
diff --git a/package/src/components/ui/Badge/RetryBadge.tsx b/package/src/components/ui/Badge/RetryBadge.tsx
new file mode 100644
index 0000000000..2b6876bca3
--- /dev/null
+++ b/package/src/components/ui/Badge/RetryBadge.tsx
@@ -0,0 +1,76 @@
+import React, { useMemo } from 'react';
+import { StyleProp, StyleSheet, View, ViewProps, ViewStyle } from 'react-native';
+
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { RotateCircle } from '../../../icons/RotateCircle';
+import { primitives } from '../../../theme';
+
+const sizes = {
+ lg: {
+ height: 32,
+ width: 32,
+ },
+ md: {
+ height: 24,
+ width: 24,
+ },
+};
+
+const iconSizes = {
+ lg: {
+ height: 16,
+ width: 16,
+ },
+ md: {
+ height: 12,
+ width: 12,
+ },
+};
+
+export type RetryBadgeProps = ViewProps & {
+ /**
+ * The size of the badge
+ * @default 'md'
+ * @type {'lg' | 'md'}
+ */
+ size: 'lg' | 'md';
+ /**
+ * The style of the badge
+ * @default undefined
+ * @type {StyleProp}
+ */
+ style?: StyleProp;
+};
+
+export const RetryBadge = (props: RetryBadgeProps) => {
+ const { size = 'md', style, ...rest } = props;
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+ return (
+
+
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: primitives.radiusMax,
+ backgroundColor: semantics.badgeBgError,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx
index 52910b4088..53adab5444 100644
--- a/package/src/contexts/messagesContext/MessagesContext.tsx
+++ b/package/src/contexts/messagesContext/MessagesContext.tsx
@@ -25,8 +25,6 @@ import type { FileIconProps } from '../../components/Attachment/FileIcon';
import type { GalleryProps } from '../../components/Attachment/Gallery';
import type { GiphyProps } from '../../components/Attachment/Giphy';
import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachment/ImageLoadingFailedIndicator';
-import type { ImageLoadingIndicatorProps } from '../../components/Attachment/ImageLoadingIndicator';
-import { ImageReloadIndicatorProps } from '../../components/Attachment/ImageReloadIndicator';
import type { URLPreviewProps } from '../../components/Attachment/UrlPreview';
import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail';
import type {
@@ -162,15 +160,10 @@ export type MessagesContextValue = Pick;
- /**
- * The indicator rendered at the center of an image whenever its loading fails, used to trigger retries.
- */
- ImageReloadIndicator: React.ComponentType;
-
/**
* The indicator rendered when image is loading. By default renders
*/
- ImageLoadingIndicator: React.ComponentType;
+ ImageLoadingIndicator: React.ComponentType;
/**
* When true, messageList will be scrolled at first unread message, when opened.
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index a5b2e9a131..171694ef11 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -594,7 +594,6 @@ export type Theme = {
container: ViewStyle;
containerInner: ViewStyle;
contentContainer: ViewStyle;
- textWrapper: ViewStyle;
editedTimestampContainer: ViewStyle;
errorContainer: ViewStyle;
errorIcon: IconProps;
@@ -1482,7 +1481,6 @@ export const defaultTheme: Theme = {
container: {},
containerInner: {},
contentContainer: {},
- textWrapper: {},
editedTimestampContainer: {},
errorContainer: {
paddingRight: 12,
diff --git a/package/src/hooks/useLoadingImage.tsx b/package/src/hooks/useLoadingImage.tsx
index 98b8bdda75..bb7d44022e 100644
--- a/package/src/hooks/useLoadingImage.tsx
+++ b/package/src/hooks/useLoadingImage.tsx
@@ -15,14 +15,23 @@ type Action =
function reducer(prevState: ImageState, action: Action) {
switch (action.type) {
case 'reloadImage':
+ if (prevState.isLoadingImage && !prevState.isLoadingImageError) {
+ return prevState;
+ }
return {
...prevState,
isLoadingImage: true,
isLoadingImageError: false,
};
case 'setLoadingImage':
+ if (prevState.isLoadingImage === action.isLoadingImage) {
+ return prevState;
+ }
return { ...prevState, isLoadingImage: action.isLoadingImage };
case 'setLoadingImageError':
+ if (prevState.isLoadingImageError === action.isLoadingImageError) {
+ return prevState;
+ }
return { ...prevState, isLoadingImageError: action.isLoadingImageError };
default:
return prevState;
@@ -30,7 +39,7 @@ function reducer(prevState: ImageState, action: Action) {
}
export const useLoadingImage = () => {
const [imageState, dispatch] = useReducer(reducer, {
- isLoadingImage: true,
+ isLoadingImage: false,
isLoadingImageError: false,
});
const { isLoadingImage, isLoadingImageError } = imageState;
diff --git a/package/src/native.ts b/package/src/native.ts
index c05e7f43de..ec60c149ef 100644
--- a/package/src/native.ts
+++ b/package/src/native.ts
@@ -1,5 +1,11 @@
import type React from 'react';
-import { FlatList as DefaultFlatList, StyleProp, ViewStyle } from 'react-native';
+import {
+ ColorValue,
+ FlatList as DefaultFlatList,
+ StyleProp,
+ ViewProps,
+ ViewStyle,
+} from 'react-native';
import type { File } from './types/types';
const fail = () => {
@@ -287,6 +293,12 @@ export type VideoType = {
rate?: number;
};
+export type NativeShimmerViewProps = ViewProps & {
+ baseColor?: ColorValue;
+ enabled?: boolean;
+ gradientColor?: ColorValue;
+};
+
type Handlers = {
Audio?: AudioType;
compressImage?: CompressImage;
@@ -306,12 +318,16 @@ type Handlers = {
setClipboardString?: SetClipboardString;
shareImage?: ShareImage;
Sound?: SoundType;
+ NativeShimmerView?: React.ComponentType;
takePhoto?: TakePhoto;
triggerHaptic?: TriggerHaptic;
Video?: React.ComponentType;
};
-export const NativeHandlers: Pick &
+export const NativeHandlers: Pick<
+ Handlers,
+ 'Audio' | 'FlatList' | 'NativeShimmerView' | 'Video' | 'Sound'
+> &
Required<
Pick<
Handlers,
@@ -346,6 +362,7 @@ export const NativeHandlers: Pick {
NativeHandlers.Sound = handlers.Sound;
}
+ if (handlers.NativeShimmerView !== undefined) {
+ NativeHandlers.NativeShimmerView = handlers.NativeShimmerView;
+ }
+
if (handlers.takePhoto !== undefined) {
NativeHandlers.takePhoto = handlers.takePhoto;
}