diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index cc5f6f072f..4254941dd8 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -21,6 +21,8 @@
+
+
diff --git a/ios/Podfile b/ios/Podfile
index 088c84cf4f..312878c51b 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -81,6 +81,10 @@ abstract_target 'Status' do
pod 'SQLCipher', '~>3.0'
pod 'SSZipArchive'
+ permissions_path = '../node_modules/react-native-permissions/ios'
+ pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone.podspec"
+ pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
+
target 'StatusIm' do
target 'StatusImTests' do
inherit! :complete
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index a724456a75..b5b6f4af25 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -70,6 +70,10 @@ PODS:
- OpenSSL-Universal (1.0.2.19):
- OpenSSL-Universal/Static (= 1.0.2.19)
- OpenSSL-Universal/Static (1.0.2.19)
+ - Permission-Camera (2.1.5):
+ - RNPermissions
+ - Permission-Microphone (2.1.5):
+ - RNPermissions
- RCTRequired (0.62.2)
- RCTTypeSafety (0.62.2):
- FBLazyVector (= 0.62.2)
@@ -255,6 +259,8 @@ PODS:
- React
- react-native-shake (3.4.0):
- React
+ - react-native-slider (3.0.0):
+ - React
- react-native-splash-screen (3.2.0):
- React
- react-native-webview (10.3.1):
@@ -318,6 +324,8 @@ PODS:
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- ReactCommon/callinvoker (= 0.62.2)
+ - ReactNativeAudioToolkit (2.0.3):
+ - React
- ReactNativeDarkMode (0.2.2):
- React
- RNCClipboard (1.2.2):
@@ -341,6 +349,8 @@ PODS:
- React
- RNLanguages (3.0.2):
- React
+ - RNPermissions (2.1.5):
+ - React
- RNReactNativeHapticFeedback (1.9.0):
- React
- RNReanimated (1.8.0):
@@ -354,8 +364,8 @@ PODS:
- SQLCipher/common (3.4.2)
- SQLCipher/standard (3.4.2):
- SQLCipher/common
- - SSZipArchive (2.2.2)
- - TOCropViewController (2.5.2)
+ - SSZipArchive (2.2.3)
+ - TOCropViewController (2.5.3)
- TouchID (4.4.1):
- React
- Yoga (1.14.0)
@@ -387,6 +397,8 @@ DEPENDENCIES:
- FlipperKit/SKIOSNetworkPlugin (~> 0.37.0)
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
+ - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera.podspec`)
+ - Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
@@ -406,6 +418,7 @@ DEPENDENCIES:
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-shake (from `../node_modules/react-native-shake`)
+ - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@@ -419,6 +432,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
+ - "ReactNativeAudioToolkit (from `../node_modules/@react-native-community/audio-toolkit`)"
- ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`)
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
@@ -427,6 +441,7 @@ DEPENDENCIES:
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLanguages (from `../node_modules/react-native-languages`)
+ - RNPermissions (from `../node_modules/react-native-permissions`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
@@ -437,11 +452,8 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
- https://github.com/CocoaPods/Specs.git:
- - boost-for-react-native
- - SQLCipher
- - SSZipArchive
trunk:
+ - boost-for-react-native
- CocoaAsyncSocket
- CocoaLibEvent
- Flipper
@@ -452,6 +464,8 @@ SPEC REPOS:
- Flipper-RSocket
- FlipperKit
- OpenSSL-Universal
+ - SQLCipher
+ - SSZipArchive
- TOCropViewController
- YogaKit
@@ -466,6 +480,10 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec"
glog:
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
+ Permission-Camera:
+ :path: "../node_modules/react-native-permissions/ios/Camera.podspec"
+ Permission-Microphone:
+ :path: "../node_modules/react-native-permissions/ios/Microphone.podspec"
RCTRequired:
:path: "../node_modules/react-native/Libraries/RCTRequired"
RCTTypeSafety:
@@ -500,6 +518,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-shake:
:path: "../node_modules/react-native-shake"
+ react-native-slider:
+ :path: "../node_modules/@react-native-community/slider"
react-native-splash-screen:
:path: "../node_modules/react-native-splash-screen"
react-native-webview:
@@ -524,6 +544,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/Vibration"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
+ ReactNativeAudioToolkit:
+ :path: "../node_modules/@react-native-community/audio-toolkit"
ReactNativeDarkMode:
:path: "../node_modules/react-native-dark-mode"
RNCClipboard:
@@ -540,6 +562,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keychain"
RNLanguages:
:path: "../node_modules/react-native-languages"
+ RNPermissions:
+ :path: "../node_modules/react-native-permissions"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
@@ -568,8 +592,10 @@ SPEC CHECKSUMS:
Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7
FlipperKit: afd4259ef9eadeeb2d30250b37d95cb3b6b97a69
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
- glog: 1f3da668190260b06b429bb211bfbee5cd790c28
+ glog: 682164e7ac67e41afd8f7b6a37a96d04caf61cc0
OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355
+ Permission-Camera: afad27bf90337684d4a86f3825112d648c8c4d3b
+ Permission-Microphone: 0ffabc3fe1c75cfb260525ee3f529383c9f4368c
RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035
RCTTypeSafety: 93006131180074cffa227a1075802c89a49dd4ce
React: 29a8b1a02bd764fb7644ef04019270849b9a7ac3
@@ -587,6 +613,7 @@ SPEC CHECKSUMS:
react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9
react-native-safe-area-context: 60f654e00b6cc416573f6d5dbfce3839958eb57a
react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c
+ react-native-slider: 12bd76d3d568c9c5500825db54123d44b48e4ad4
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webview: 40bbeb6d011226f34cb83f845aeb0fdf515cfc5f
React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c
@@ -599,6 +626,7 @@ SPEC CHECKSUMS:
React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d
React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256
ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3
+ ReactNativeAudioToolkit: de9610f323e855ac6574be8c99621f3d57c5df06
ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949
RNCClipboard: 8148e21ac347c51fd6cd4b683389094c216bb543
RNCMaskedView: 71fc32d971f03b7f03d6ab6b86b730c4ee64f5b6
@@ -607,17 +635,18 @@ SPEC CHECKSUMS:
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
RNKeychain: 216f37338fcb9e5c3a2530f1e3295f737a690cb1
RNLanguages: 962e562af0d34ab1958d89bcfdb64fafc37c513e
+ RNPermissions: ad71dd4f767ec254f2cd57592fbee02afee75467
RNReactNativeHapticFeedback: 2566b468cc8d0e7bb2f84b23adc0f4614594d071
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
RNScreens: ac02d0e4529f08ced69f5580d416f968a6ec3a1d
RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f
SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990
- SSZipArchive: fa16b8cc4cdeceb698e5e5d9f67e9558532fbf23
- TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
+ SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
+ TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6
TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4
Yoga: 3ebccbdd559724312790e7742142d062476b698e
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
-PODFILE CHECKSUM: f66349c5bfb9c21ac968307ea5a2d6c2dd4091ed
+PODFILE CHECKSUM: 5faa578ff5cb7a30abc18b9d620df288750a72fe
COCOAPODS: 1.9.1
diff --git a/ios/StatusIm/Info.plist b/ios/StatusIm/Info.plist
index 573ee89cb6..559a216a7c 100644
--- a/ios/StatusIm/Info.plist
+++ b/ios/StatusIm/Info.plist
@@ -83,6 +83,8 @@
Location access is required for some DApps to function properly.
NSPhotoLibraryUsageDescription
Photos access is required to give you the ability to send images.
+ NSMicrophoneUsageDescription
+ Need microphone access for sending audio messages.
UIAppFonts
Inter-Bold.otf
diff --git a/ios/StatusImPR/Info.plist b/ios/StatusImPR/Info.plist
index 02441fc97d..05ec5f0ef1 100644
--- a/ios/StatusImPR/Info.plist
+++ b/ios/StatusImPR/Info.plist
@@ -83,6 +83,8 @@
Location access is required for some DApps to function properly.
NSPhotoLibraryUsageDescription
Photos access is required to give you the ability to send images.
+ NSMicrophoneUsageDescription
+ Need microphone access for sending audio messages.
UIAppFonts
Inter-Bold.otf
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
index 2faf6f8b63..507573c0a0 100644
--- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
@@ -1346,5 +1346,33 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r);
}
+
+ @ReactMethod
+ public void activateKeepAwake() {
+ final Activity activity = getCurrentActivity();
+
+ if (activity != null) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ });
+ }
+ }
+
+ @ReactMethod
+ public void deactivateKeepAwake() {
+ final Activity activity = getCurrentActivity();
+
+ if (activity != null) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ });
+ }
+ }
}
diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
index b5add0f2b3..edd8769586 100644
--- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m
+++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
@@ -749,6 +749,20 @@ RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue)
[userDefaults synchronize];
}
+RCT_EXPORT_METHOD(activateKeepAwake)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
+ });
+}
+
+RCT_EXPORT_METHOD(deactivateKeepAwake)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[UIApplication sharedApplication] setIdleTimerDisabled:NO];
+ });
+}
+
//// deviceinfo
- (bool) is24Hour
diff --git a/nix/deps/gradle/deps.json b/nix/deps/gradle/deps.json
index 5476a1d9db..2e403a9e20 100644
--- a/nix/deps/gradle/deps.json
+++ b/nix/deps/gradle/deps.json
@@ -5794,16 +5794,16 @@
},
{
- "path": "com/google/protobuf/protobuf-java/3.12.2/protobuf-java-3.12.2",
+ "path": "com/google/protobuf/protobuf-java/4.0.0-rc-2/protobuf-java-4.0.0-rc-2",
"host": "https://repo.maven.apache.org/maven2",
"type": "jar",
"pom": {
- "sha1": "da3bb9dce2a722503f0d3e4f3b5eea93f737ba48",
- "sha256": "1ijicnm83gq33px9c5r4man0n2zw6axrxsqw0jnm1miixyjz08cv"
+ "sha1": "8fab0fe0746b1d370663a3db7dd483c2c4d34a6f",
+ "sha256": "17agfgbdfxdk304fgzk0iwsf7ggwqrwb44b3nnjc0xjdgqlxmzxb"
},
"jar": {
- "sha1": "f0029524002295154c6b546d4e6bd1cfc5081874",
- "sha256": "186a1lgmcfr3iml7bfxk9gnbsafyxmriqfyglvkwa85d7gwvx1dk"
+ "sha1": "ae590f34383d2d6fde2b3b8e2ffb668e64579099",
+ "sha256": "0al23s9s97sxlrrndb91cnra07fdchbcgvhcacyd6yjzysz55hiw"
}
},
@@ -5828,12 +5828,12 @@
},
{
- "path": "com/google/protobuf/protobuf-parent/3.12.2/protobuf-parent-3.12.2",
+ "path": "com/google/protobuf/protobuf-parent/4.0.0-rc-2/protobuf-parent-4.0.0-rc-2",
"host": "https://repo.maven.apache.org/maven2",
"type": "pom",
"pom": {
- "sha1": "f5b5d4cb5e6d512f2fac5d2b08e599644a53c087",
- "sha256": "0md51xi5af8yi9l1dpqbmysrvlll05zpvmalag6p4f6fykwcry05"
+ "sha1": "c5391c7917ca1197cbbda4efc6454a87e7e7107e",
+ "sha256": "0bvynr52xjyd42n23ih635h2cmyillfbac0yjymhlp505f0p1lp0"
}
},
diff --git a/nix/deps/gradle/deps.urls b/nix/deps/gradle/deps.urls
index abd783de15..3140123e67 100644
--- a/nix/deps/gradle/deps.urls
+++ b/nix/deps/gradle/deps.urls
@@ -417,10 +417,10 @@ https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs/1.1/jimfs-1.1.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.4.0/protobuf-java-util-3.4.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.0.0/protobuf-java-3.0.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.4.0/protobuf-java-3.4.0.pom
-https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.12.2/protobuf-java-3.12.2.pom
+https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/4.0.0-rc-2/protobuf-java-4.0.0-rc-2.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.0.0/protobuf-parent-3.0.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.4.0/protobuf-parent-3.4.0.pom
-https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.12.2/protobuf-parent-3.12.2.pom
+https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/4.0.0-rc-2/protobuf-parent-4.0.0-rc-2.pom
https://repo.maven.apache.org/maven2/com/google/truth/truth-parent/1.0.1/truth-parent-1.0.1.pom
https://repo.maven.apache.org/maven2/com/google/truth/truth/1.0.1/truth-1.0.1.pom
https://repo.maven.apache.org/maven2/com/google/zxing/core/3.3.3/core-3.3.3.pom
diff --git a/nix/deps/gradle/proj.list b/nix/deps/gradle/proj.list
index ec409cb12c..04197555e2 100644
--- a/nix/deps/gradle/proj.list
+++ b/nix/deps/gradle/proj.list
@@ -1,10 +1,12 @@
app
react-native-background-timer
react-native-camera
+react-native-community_audio-toolkit
react-native-community_cameraroll
react-native-community_clipboard
react-native-community_masked-view
react-native-community_netinfo
+react-native-community_slider
react-native-config
react-native-dark-mode
react-native-dialogs
@@ -18,6 +20,7 @@ react-native-languages
react-native-mail
react-native-navigation-bar-color
react-native-nfc-manager
+react-native-permissions
react-native-reanimated
react-native-safe-area-context
react-native-screens
diff --git a/package.json b/package.json
index a56e8235a8..cda4775e5b 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,13 @@
"app:android": "react-native run-android"
},
"dependencies": {
+ "@react-native-community/audio-toolkit": "git+https://github.com/tbenr/react-native-audio-toolkit.git#v2.0.3-status-v6",
"@react-native-community/cameraroll": "^1.6.1",
"@react-native-community/clipboard": "^1.2.2",
"@react-native-community/hooks": "^2.5.1",
"@react-native-community/masked-view": "^0.1.6",
"@react-native-community/netinfo": "^4.4.0",
+ "@react-native-community/slider": "^3.0.0",
"@react-navigation/bottom-tabs": "^5.7.0",
"@react-navigation/native": "^5.7.0",
"@react-navigation/stack": "^5.7.0",
@@ -46,6 +48,7 @@
"react-native-languages": "^3.0.2",
"react-native-mail": "git+https://github.com/status-im/react-native-mail.git#v4.0.0-status",
"react-native-navigation-bar-color": "^2.0.1",
+ "react-native-permissions": "^2.1.5",
"react-native-reanimated": "^1.7.0",
"react-native-redash": "^14.2.2",
"react-native-safe-area-context": "^2.0.0",
diff --git a/resources/images/icons/pause@2x.png b/resources/images/icons/pause@2x.png
new file mode 100755
index 0000000000..83ceee86e3
Binary files /dev/null and b/resources/images/icons/pause@2x.png differ
diff --git a/resources/images/icons/pause@3x.png b/resources/images/icons/pause@3x.png
new file mode 100755
index 0000000000..8691dd1676
Binary files /dev/null and b/resources/images/icons/pause@3x.png differ
diff --git a/resources/images/icons/play@2x.png b/resources/images/icons/play@2x.png
new file mode 100755
index 0000000000..58785d143b
Binary files /dev/null and b/resources/images/icons/play@2x.png differ
diff --git a/resources/images/icons/play@3x.png b/resources/images/icons/play@3x.png
new file mode 100755
index 0000000000..6d05bb410a
Binary files /dev/null and b/resources/images/icons/play@3x.png differ
diff --git a/resources/images/icons/speech@2x.png b/resources/images/icons/speech@2x.png
new file mode 100644
index 0000000000..45f36003b3
Binary files /dev/null and b/resources/images/icons/speech@2x.png differ
diff --git a/resources/images/icons/speech@3x.png b/resources/images/icons/speech@3x.png
new file mode 100644
index 0000000000..93c7c09ed0
Binary files /dev/null and b/resources/images/icons/speech@3x.png differ
diff --git a/src/status_im/audio/core.cljs b/src/status_im/audio/core.cljs
new file mode 100644
index 0000000000..a318f7fea3
--- /dev/null
+++ b/src/status_im/audio/core.cljs
@@ -0,0 +1,129 @@
+(ns status-im.audio.core
+ (:require ["@react-native-community/audio-toolkit" :refer (Player Recorder MediaStates)]))
+
+;; get mediastates from react module
+(def PLAYING (.-PLAYING ^js MediaStates))
+(def PAUSED (.-PAUSED ^js MediaStates))
+(def RECORDING (.-RECORDING ^js MediaStates))
+(def PREPARED (.-PREPARED ^js MediaStates))
+(def IDLE (.-IDLE ^js MediaStates))
+(def ERROR (.-ERROR ^js MediaStates))
+(def DESTROYED (.-DESTROYED ^js MediaStates))
+(def SEEKING (.-SEEKING ^js MediaStates))
+
+(def default-recorder-options {:filename "recording.aac"
+ :bitrate 32000
+ :channels 1
+ :sampleRate 22050
+ :quality "medium" ; ios only
+ :meteringInterval 50})
+
+(defn get-state [player-recorder]
+ (when player-recorder
+ (.-state ^js player-recorder)))
+
+(defn new-recorder [options on-meter on-ended]
+ (let [recorder (new ^js Recorder
+ (:filename options)
+ (clj->js options))]
+ (when on-meter
+ (.on ^js recorder "meter" on-meter))
+ (when on-ended
+ (.on ^js recorder "ended" on-ended))))
+
+(defn new-player [audio options on-ended]
+ (let [player (new ^js Player
+ audio
+ (clj->js options))]
+ (when on-ended
+ (.on ^js player "ended" on-ended))))
+
+(defn prepare-player [player on-prepared on-error]
+ (when (and player (.-canPrepare ^js player))
+ (.prepare ^js player #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-prepared)))))
+
+(defn prepare-recorder [recorder on-prepared on-error]
+ (when (and recorder (.-canPrepare ^js recorder))
+ (.prepare ^js recorder (fn [err _]
+ (if err
+ (on-error {:error (.-err err) :message (.-message err)})
+ (on-prepared))))))
+
+(defn start-recording [recorder on-start on-error]
+ (when (and recorder
+ (or
+ (.-canRecord ^js recorder)
+ (.-canPrepare ^js recorder)))
+ (.record ^js recorder #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-start)))))
+
+(defn stop-recording [recorder on-stop on-error]
+ (if (and recorder (#{RECORDING PAUSED} (get-state recorder)))
+ (.stop ^js recorder #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-stop)))
+ (on-stop)))
+
+(defn pause-recording [recorder on-pause on-error]
+ (when (and recorder (.-isRecording ^js recorder))
+ (.pause ^js recorder #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-pause)))))
+
+(defn start-playing [player on-start on-error]
+ (when (and player (.-canPlay ^js player))
+ (.play ^js player #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-start)))))
+
+(defn stop-playing [player on-stop on-error]
+ (if (and player (.-isPlaying ^js player))
+ (.stop ^js player #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-stop)))
+ (on-stop)))
+
+(defn get-recorder-file-path [recorder]
+ (when recorder
+ (.-fsPath ^js recorder)))
+
+(defn get-player-duration [player]
+ (when (and player (.-canPlay ^js player))
+ (.-duration ^js player)))
+
+(defn get-player-current-time [player]
+ (when (and player (.-canPlay ^js player))
+ (.-currentTime ^js player)))
+
+(defn toggle-playpause-player [player on-play on-pause on-error]
+ (when (and player (.-canPlay ^js player))
+ (.playPause ^js player (fn [error pause?]
+ (if error
+ (on-error {:error (.-err error) :message (.-message error)})
+ (if pause?
+ (on-pause)
+ (on-play)))))))
+
+(defn seek-player [player value on-seek on-error]
+ (when (and player (.-canPlay ^js player))
+ (.seek ^js player value #(if %
+ (on-error {:error (.-err %) :message (.-message %)})
+ (on-seek)))))
+
+(defn can-play? [player]
+ (and player (.-canPlay ^js player)))
+
+(defn destroy-recorder [recorder]
+ (stop-recording recorder
+ #(when (and recorder (not= (get-state recorder) DESTROYED))
+ (.destroy ^js recorder))
+ #()))
+
+(defn destroy-player [player]
+ (stop-playing player
+ #(when (and player (not= (get-state player) IDLE))
+ (.destroy ^js player))
+ #()))
\ No newline at end of file
diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs
index e3d553a34e..0d0ca9a47c 100644
--- a/src/status_im/chat/models/input.cljs
+++ b/src/status_im/chat/models/input.cljs
@@ -6,6 +6,7 @@
[status-im.chat.models.message :as chat.message]
[status-im.chat.models.message-content :as message-content]
[status-im.constants :as constants]
+ [status-im.i18n :as i18n]
[status-im.utils.datetime :as datetime]
[status-im.utils.fx :as fx]
["emojilib" :as emojis]))
@@ -106,7 +107,16 @@
(chat.message/send-message {:chat-id current-chat-id
:content-type constants/content-type-image
:image-path (string/replace image-path #"file://" "")
- :text "Update to latest version to see a nice image here!"})))))
+ :text (i18n/label :t/update-to-see-image)})))))
+
+(fx/defn send-audio-message
+ [cofx audio-path duration current-chat-id]
+ (when-not (string/blank? audio-path)
+ (chat.message/send-message cofx {:chat-id current-chat-id
+ :content-type constants/content-type-audio
+ :audio-path audio-path
+ :audio-duration-ms duration
+ :text (i18n/label :t/update-to-listen-audio)})))
(fx/defn send-sticker-message
[cofx {:keys [hash pack]} current-chat-id]
@@ -115,7 +125,7 @@
:content-type constants/content-type-sticker
:sticker {:hash hash
:pack pack}
- :text "Update to latest version to see a nice sticker here!"})))
+ :text (i18n/label :t/update-to-see-sticker)})))
(fx/defn send-current-message
"Sends message from current chat input"
diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs
index bbe3382f9d..73b38abf70 100644
--- a/src/status_im/constants.cljs
+++ b/src/status_im/constants.cljs
@@ -14,6 +14,7 @@
(def content-type-command 5)
(def content-type-system-text 6)
(def content-type-image 7)
+(def content-type-audio 8)
(def message-type-one-to-one 1)
(def message-type-public-group 2)
diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs
index c4a9ba5f41..5c2a222dfc 100644
--- a/src/status_im/data_store/messages.cljs
+++ b/src/status_im/data_store/messages.cljs
@@ -24,7 +24,8 @@
:contentType :content-type
:clock :clock-value
:quotedMessage :quoted-message
- :outgoingStatus :outgoing-status})
+ :outgoingStatus :outgoing-status
+ :audioDurationMs :audio-duration-ms})
(update :outgoing-status keyword)
(update :command-parameters clojure.set/rename-keys {:transactionHash :transaction-hash
diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs
index cf3295c578..1081c42499 100644
--- a/src/status_im/events.cljs
+++ b/src/status_im/events.cljs
@@ -560,6 +560,11 @@
{})
(chat.input/send-sticker-message sticker current-chat-id))))
+(handlers/register-handler-fx
+ :chat/send-audio
+ (fn [{{:keys [current-chat-id]} :db :as cofx} [_ audio-path duration]]
+ (chat.input/send-audio-message cofx audio-path duration current-chat-id)))
+
(handlers/register-handler-fx
:chat/disable-cooldown
(fn [cofx _]
@@ -1238,7 +1243,8 @@
(fx/defn on-going-in-background [{:keys [db now]}]
{:db (-> db
(dissoc :app-active-since)
- (assoc :app-in-background-since now))})
+ (assoc :app-in-background-since now))
+ :dispatch-n [[:audio-recorder/on-background] [:audio-message/on-background]]})
(defn app-state-change [state {:keys [db] :as cofx}]
(let [app-coming-from-background? (= state "active")
diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs
index 97d2b92b8c..f79dc81d8c 100644
--- a/src/status_im/native_module/core.cljs
+++ b/src/status_im/native_module/core.cljs
@@ -364,3 +364,11 @@
[key-uid callback]
(log/debug "[native-module] delete-multiaccount")
(.deleteMultiaccount ^js (status) key-uid callback))
+
+(defn activate-keep-awake []
+ (log/debug "[native-module] activateKeepAwake")
+ (.activateKeepAwake ^js (status)))
+
+(defn deactivate-keep-awake []
+ (log/debug "[native-module] deactivateKeepAwake")
+ (.deactivateKeepAwake ^js (status)))
diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs
index eb5a5c6977..1dddfb4588 100644
--- a/src/status_im/transport/message/protocol.cljs
+++ b/src/status_im/transport/message/protocol.cljs
@@ -10,6 +10,8 @@
response-to
ens-name
image-path
+ audio-path
+ audio-duration-ms
message-type
sticker
content-type]
@@ -22,6 +24,8 @@
:responseTo response-to
:ensName ens-name
:imagePath image-path
+ :audioPath audio-path
+ :audioDurationMs audio-duration-ms
:sticker sticker
:contentType content-type}]
:on-success
diff --git a/src/status_im/ui/components/colors.cljs b/src/status_im/ui/components/colors.cljs
index 410d1c56a2..3856ecc879 100644
--- a/src/status_im/ui/components/colors.cljs
+++ b/src/status_im/ui/components/colors.cljs
@@ -66,6 +66,7 @@
;; RED
(def red (:red light)) ;; Used to highlight errors or "dangerous" actions
(def red-transparent-10 (alpha red 0.1)) ;;action-row ;; ttt finish
+(def red-audio-recorder "#fa6565")
;; GREEN
(def green "#44d058") ;; icon for successful inboud transaction
diff --git a/src/status_im/ui/components/permissions.cljs b/src/status_im/ui/components/permissions.cljs
index cb85e58401..26132ea364 100644
--- a/src/status_im/ui/components/permissions.cljs
+++ b/src/status_im/ui/components/permissions.cljs
@@ -1,26 +1,33 @@
(ns status-im.ui.components.permissions
(:require [status-im.utils.platform :as platform]
- ["react-native" :refer (PermissionsAndroid)]))
+ ["react-native-permissions" :refer (requestMultiple PERMISSIONS RESULTS)]))
(def permissions-map
- {:read-external-storage "android.permission.READ_EXTERNAL_STORAGE"
- :write-external-storage "android.permission.WRITE_EXTERNAL_STORAGE"
- :camera "android.permission.CAMERA"})
+ {:read-external-storage (cond
+ platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
+ :write-external-storage (cond
+ platform/android? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
+ :camera (cond
+ platform/android? (.-CAMERA (.-ANDROID PERMISSIONS))
+ platform/ios? (.-CAMERA (.-IOS PERMISSIONS)))
+ :record-audio (cond
+ platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS))
+ platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))})
(defn all-granted? [permissions]
(let [permission-vals (distinct (vals permissions))]
(and (= (count permission-vals) 1)
- (not (#{"denied" "never_ask_again"} (first permission-vals))))))
+ (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals))))))
(defn request-permissions
[{:keys [permissions on-allowed on-denied]
:or {on-allowed #()
on-denied #()}}]
- (if platform/android?
- (let [permissions (mapv #(get permissions-map %) permissions)]
- (-> (.requestMultiple PermissionsAndroid (clj->js permissions))
+ (let [permissions (remove nil? (mapv #(get permissions-map %) permissions))]
+ (if (empty? permissions)
+ (on-allowed)
+ (-> (requestMultiple (clj->js permissions))
(.then #(if (all-granted? (js->clj %))
(on-allowed)
(on-denied)))
- (.catch on-denied)))
- (on-allowed)))
+ (.catch on-denied)))))
diff --git a/src/status_im/ui/components/slider.cljs b/src/status_im/ui/components/slider.cljs
new file mode 100644
index 0000000000..90ee45a35c
--- /dev/null
+++ b/src/status_im/ui/components/slider.cljs
@@ -0,0 +1,9 @@
+(ns status-im.ui.components.slider
+ (:require [reagent.core :as reagent]
+ ["react-native" :refer (Animated)]
+ ["@react-native-community/slider" :default Slider]))
+
+(def slider (reagent/adapt-react-class Slider))
+
+(def animated-slider
+ (reagent/adapt-react-class (.createAnimatedComponent Animated Slider)))
\ No newline at end of file
diff --git a/src/status_im/ui/screens/chat/audio_message/styles.cljs b/src/status_im/ui/screens/chat/audio_message/styles.cljs
new file mode 100644
index 0000000000..881c175b04
--- /dev/null
+++ b/src/status_im/ui/screens/chat/audio_message/styles.cljs
@@ -0,0 +1,48 @@
+(ns status-im.ui.screens.chat.audio-message.styles
+ (:require [status-im.ui.components.colors :as colors]))
+
+(def container
+ {:flex 1
+ :flex-direction :column
+ :justify-content :space-around})
+
+(def timer
+ {:font-size 28
+ :line-height 38
+ :align-self :center})
+
+(def buttons-container
+ {:flex 1
+ :max-height 80
+ :flex-direction :row
+ :align-items :center
+ :justify-content :space-around
+ :align-self :stretch
+ :padding-horizontal 80})
+
+(def rec-button-base-size 61)
+
+(def rec-button-container
+ {:width rec-button-base-size
+ :height rec-button-base-size
+ :align-items "center"})
+
+(defn rec-outer-circle [scale-anim]
+ {:position "absolute"
+ :width rec-button-base-size
+ :height rec-button-base-size
+ :top 0
+ :border-width 4
+ :transform [{:scale scale-anim}]
+ :border-color colors/red-audio-recorder
+ :border-radius rec-button-base-size})
+
+(defn rec-inner-circle [scale-anim border-radius-anim]
+ {:position "absolute"
+ :top 6
+ :left 6
+ :bottom 6
+ :right 6
+ :transform [{:scale scale-anim}]
+ :border-radius border-radius-anim
+ :background-color colors/red-audio-recorder})
\ No newline at end of file
diff --git a/src/status_im/ui/screens/chat/audio_message/views.cljs b/src/status_im/ui/screens/chat/audio_message/views.cljs
new file mode 100644
index 0000000000..422531448e
--- /dev/null
+++ b/src/status_im/ui/screens/chat/audio_message/views.cljs
@@ -0,0 +1,292 @@
+(ns status-im.ui.screens.chat.audio-message.views
+ (:require-macros [status-im.utils.views :refer [defview letsubs]])
+ (:require
+ [goog.string :as gstring]
+ [reagent.core :as reagent]
+ [status-im.audio.core :as audio]
+ [status-im.ui.components.react :as react]
+ [re-frame.core :as re-frame]
+ [status-im.i18n :as i18n]
+ [quo.components.animated.pressable :as pressable]
+ [status-im.native-module.core :as status]
+ [status-im.ui.screens.chat.components.input :as input]
+ [status-im.ui.screens.chat.components.style :as input.style]
+ [status-im.ui.screens.chat.audio-message.styles :as styles]
+ [status-im.ui.components.colors :as colors]
+ [status-im.ui.components.animation :as anim]
+ [status-im.ui.components.icons.vector-icons :as icons]
+ [status-im.utils.utils :as utils.utils]
+ [status-im.utils.fs :as fs]
+ [status-im.utils.fx :as fx]))
+
+;; reference db levels
+(def total-silence-db -160)
+(def silence-db -35)
+(def max-db 0)
+
+;; update interval for the pulsing rec button
+(def metering-interval 100)
+
+;; rec pulse animation target
+(defonce visual-target-value (anim/create-value total-silence-db))
+;;ensure animation finishes before next meter update
+(defonce metering-anim-duration (int (* metering-interval 0.9)))
+
+(defn update-meter [meter-data]
+ (let [value (if meter-data
+ (.-value ^js meter-data)
+ total-silence-db)]
+ (anim/start (anim/timing visual-target-value {:toValue value
+ :duration metering-anim-duration
+ :useNativeDriver true}))))
+
+(def base-filename "am.")
+(def default-format "aac")
+(def rec-options (merge
+ audio/default-recorder-options
+ {:filename (str base-filename default-format)
+ :meteringInterval metering-interval}))
+
+;; maximum 2 minutes of recordings time
+;; to keep data under 900k
+(def max-recording-ms (* 2 60 1000))
+
+;; audio objects
+(defonce recorder-ref (atom nil))
+(defonce player-ref (atom nil))
+
+(defn destroy-recorder []
+ (audio/destroy-recorder @recorder-ref)
+ (reset! recorder-ref nil))
+
+(defn destroy-player []
+ (audio/destroy-player @player-ref)
+ (reset! player-ref nil))
+
+;; state update callback
+(defonce state-cb (atom #()))
+
+;; max recording ms reached callback
+(defonce max-recording-reached-cb (atom #()))
+
+;; to be called when app goes in background
+(defonce on-background-cb (atom #()))
+
+(fx/defn on-background
+ {:events [:audio-recorder/on-background]}
+ [_]
+ (when @on-background-cb
+ (@on-background-cb))
+ nil)
+
+;; during recording
+(defonce recording-timer (atom nil))
+(defonce recording-start-ts (atom nil))
+(defonce recording-backlog-ms (atom 0))
+
+;; updates timer UI
+(defn update-timer [timer]
+ (let [ms (if @recording-start-ts
+ (+
+ (- (js/Date.now) @recording-start-ts)
+ @recording-backlog-ms)
+ @recording-backlog-ms)
+ s (quot ms 1000)]
+ (if (> ms max-recording-ms)
+ (@max-recording-reached-cb)
+ (reset! timer (gstring/format "%d:%02d" (quot s 60) (mod s 60))))))
+
+(defn reset-timer [timer]
+ (reset! timer "0:00")
+ (reset! recording-backlog-ms 0))
+
+(defn animate-buttons [rec? show-ctrl? {:keys [rec-button-anim-value ctrl-buttons-anim-value]}]
+ (anim/start
+ (anim/parallel
+ [(anim/timing rec-button-anim-value {:toValue (if rec? 1 0)
+ :duration 100
+ :useNativeDriver true})
+ (anim/timing ctrl-buttons-anim-value {:toValue (if show-ctrl? 1 0)
+ :duration 100
+ :useNativeDriver true})])))
+
+(defn start-recording [{:keys [timer] :as params}]
+ (if (> @recording-backlog-ms max-recording-ms)
+ (@max-recording-reached-cb)
+ (do
+ (animate-buttons true true params)
+ (reset! recording-start-ts (js/Date.now))
+ (reset! recording-timer (utils.utils/set-interval #(update-timer timer) 1000))
+ (audio/start-recording
+ @recorder-ref
+ @state-cb
+ #(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %))))))
+
+(defn reload-recorder []
+ (when @recorder-ref
+ (destroy-recorder))
+ (reset! recorder-ref (audio/new-recorder rec-options #(update-meter %) @state-cb))
+ ;; we skip preparation since if a recorder is prepared, player wont play
+ (@state-cb))
+
+(defn reload-player
+ ([] (reload-player nil))
+ ([on-success]
+ (when @player-ref
+ (destroy-player))
+ (reset! player-ref (audio/new-player
+ (:filename rec-options)
+ {:autoDestroy false
+ :continuesToPlayInBackground false}
+ @state-cb))
+ (audio/prepare-player
+ @player-ref
+ #(do (@state-cb) (when on-success (on-success)))
+ #(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))))
+
+(defn stop-recording [{:keys [on-success timer max-recording-reached?] :as params}]
+ (when @recording-timer
+ (utils.utils/clear-interval @recording-timer)
+ (reset! recording-timer nil))
+ (if max-recording-reached?
+ (reset! recording-backlog-ms (+ @recording-backlog-ms (- (js/Date.now) @recording-start-ts)))
+ (reset-timer timer))
+ (audio/stop-recording
+ @recorder-ref
+ #(do
+ (update-meter nil)
+ (reload-recorder)
+ (reload-player on-success))
+ #(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))
+ (animate-buttons false max-recording-reached? params))
+
+(defn pause-recording [{:keys [timer] :as params}]
+ (when @recording-timer
+ (utils.utils/clear-interval @recording-timer)
+ (reset! recording-backlog-ms (+ @recording-backlog-ms (- (js/Date.now) @recording-start-ts)))
+ (reset! recording-start-ts nil)
+ (reset! recording-timer nil)
+ (update-timer timer))
+ (audio/pause-recording
+ @recorder-ref
+ #(do (update-meter nil)
+ (@state-cb))
+ #(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))
+ (animate-buttons false true params))
+
+(defn update-state
+ "update main UI state.
+ general states are:
+ - :recording
+ - :playing
+ - :ready-to-send
+ - :recording-paused
+ - :ready-to-record"
+ [state-ref]
+ (let [player-state (audio/get-state @player-ref)
+ recorder-state (audio/get-state @recorder-ref)
+ output-file (or
+ (audio/get-recorder-file-path @recorder-ref)
+ (:output-file @state-ref))
+ general (cond
+ (= recorder-state audio/RECORDING) :recording
+ (= player-state audio/PLAYING) :playing
+ (= player-state audio/PREPARED) :ready-to-send
+ (= recorder-state audio/PAUSED) :recording-paused
+ :else :ready-to-record)
+ new-state {:general general
+ :cancel-disabled? (nil? (#{:recording :recording-paused :ready-to-send} general))
+ :output-file output-file
+ :duration (audio/get-player-duration @player-ref)}]
+ (if (#{:recording :recording-paused} general)
+ (status/activate-keep-awake)
+ (status/deactivate-keep-awake))
+ (when (not= @state-ref new-state)
+ (reset! state-ref new-state))))
+
+(defn send-audio-msessage [state-ref]
+ (re-frame/dispatch [:chat/send-audio
+ (:output-file @state-ref)
+ (int (:duration @state-ref))])
+ (destroy-player)
+ (@state-cb))
+
+;; rec-button-anim-value 0 => stopped, 1 => recording
+(defview rec-button-view [{:keys [rec-button-anim-value state] :as params}]
+ (letsubs [outer-scale (anim/interpolate visual-target-value {:inputRange [total-silence-db silence-db 0]
+ :outputRange [1 0.8 1.2]})
+ inner-scale (anim/interpolate rec-button-anim-value {:inputRange [0 1]
+ :outputRange [1 0.5]})
+ inner-border-radius (anim/interpolate rec-button-anim-value {:inputRange [0 1]
+ :outputRange [styles/rec-button-base-size 16]})]
+ [react/touchable-highlight {:on-press #(if (= (:general @state) :recording)
+ (pause-recording params)
+ (start-recording params))}
+ [react/view {:style styles/rec-button-container}
+ [react/animated-view {:style (styles/rec-outer-circle outer-scale)}]
+ [react/animated-view {:style (styles/rec-inner-circle inner-scale inner-border-radius)}]]]))
+
+(defn- cancel-button [disabled? on-press]
+ [pressable/pressable {:type :scale
+ :disabled disabled?
+ :on-press on-press}
+ [react/view {:style (input.style/send-message-button)}
+ [icons/icon :main-icons/close
+ {:container-style (merge (input.style/send-message-container) {:background-color colors/gray})
+ :accessibility-label :cancel-message-button
+ :color colors/white-persist}]]])
+
+(defview audio-message-view []
+ (letsubs [rec-button-anim-value (anim/create-value 0)
+ ctrl-buttons-anim-value (anim/create-value 0)
+ timer (reagent/atom "")
+ state (reagent/atom nil)]
+ {:component-did-mount (fn []
+ (reset-timer timer)
+ (reset! state-cb #(update-state state))
+ (reset! max-recording-reached-cb #(do
+ (when (= (:general @state) :recording)
+ (stop-recording {:rec-button-anim-value rec-button-anim-value
+ :ctrl-buttons-anim-value ctrl-buttons-anim-value
+ :timer timer
+ :max-recording-reached? true}))
+ (utils.utils/show-popup (i18n/label :t/audio-recorder)
+ (i18n/label :t/audio-recorder-max-ms-reached))))
+ (reset! on-background-cb #(when (= (:general @state) :recording)
+ (pause-recording {:rec-button-anim-value rec-button-anim-value
+ :ctrl-buttons-anim-value ctrl-buttons-anim-value
+ :timer timer})))
+ (reload-recorder))
+ :component-will-unmount (fn []
+ (when @recording-timer
+ (utils.utils/clear-interval @recording-timer)
+ (reset! recording-timer nil))
+ (destroy-recorder)
+ (destroy-player)
+ (when (:output-file @state)
+ ; possible issue if message is not yet sent?
+ (fs/unlink (:output-file @state)))
+ (reset! state-cb nil)
+ (reset! max-recording-reached-cb nil)
+ (reset! on-background-cb nil))}
+ (let [base-params {:rec-button-anim-value rec-button-anim-value
+ :ctrl-buttons-anim-value ctrl-buttons-anim-value
+ :timer timer}]
+ [react/view {:style styles/container}
+ [react/text {:style styles/timer} @timer]
+ [react/view {:style styles/buttons-container}
+ [react/animated-view {:style {:opacity ctrl-buttons-anim-value}}
+ [cancel-button (:cancel-disabled? @state) #(stop-recording base-params)]]
+ [rec-button-view (merge base-params {:state state})]
+ [react/animated-view {:style {:opacity ctrl-buttons-anim-value}}
+ [input/send-button {:on-send-press (fn [] (cond
+ (= :ready-to-send (:general @state))
+ (do
+ (reset-timer timer)
+ (animate-buttons false false base-params)
+ (send-audio-msessage state))
+
+ (#{:recording :recording-paused} (:general @state))
+ (stop-recording (merge base-params
+ {:on-success
+ #(send-audio-msessage state)}))))}]]]])))
diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs
index 5cafc39f8a..1a6afdbabf 100644
--- a/src/status_im/ui/screens/chat/components/input.cljs
+++ b/src/status_im/ui/screens/chat/components/input.cljs
@@ -6,6 +6,7 @@
[quo.design-system.colors :as colors]
[status-im.ui.screens.chat.components.style :as styles]
[status-im.ui.screens.chat.components.reply :as reply]
+ [status-im.utils.utils :as utils.utils]
[quo.components.animated.pressable :as pressable]
[quo.animated :as animated]
[status-im.utils.config :as config]
@@ -37,12 +38,25 @@
[icons/icon :main-icons/keyboard (styles/icon false)]
[icons/icon :main-icons/stickers (styles/icon false)])]])
+(defn- request-record-audio-permission [set-active panel]
+ (re-frame/dispatch
+ [:request-permissions
+ {:permissions [:record-audio]
+ :on-allowed
+ #(set-active panel)
+ :on-denied
+ #(utils.utils/set-timeout
+ (fn []
+ (utils.utils/show-popup (i18n/label :t/audio-recorder-error)
+ (i18n/label :t/audio-recorder-permissions-error)))
+ 50)}]))
+
(defn touchable-audio-icon [{:keys [panel active set-active accessibility-label input-focus]}]
[pressable/pressable {:type :scale
:accessibility-label accessibility-label
:on-press #(if (= active panel)
(input-focus)
- (set-active panel))}
+ (request-record-audio-permission set-active panel))}
[rn/view {:style (styles/in-input-touchable-icon)}
[icons/icon
(panel->icons panel)
@@ -116,6 +130,7 @@
[touchable-audio-icon {:panel :audio
:accessibility-label :show-audio-message-icon
:active active-panel
+ :input-focus input-focus
:set-active set-active-panel}])]]]])
(defn chat-toolbar []
@@ -151,8 +166,8 @@
one-to-one-chat?
(or config/commands-enabled? mainnet?)
(not reply))
- show-audio (and false ;TODO: remove to enable audio icon
- empty-text
+ show-audio (and empty-text
+ (not sending-image)
(not reply)
(not public?))]
(when-not (= reply @had-reply)
diff --git a/src/status_im/ui/screens/chat/components/style.cljs b/src/status_im/ui/screens/chat/components/style.cljs
index 0fb7da4542..1cba33a325 100644
--- a/src/status_im/ui/screens/chat/components/style.cljs
+++ b/src/status_im/ui/screens/chat/components/style.cljs
@@ -103,7 +103,8 @@
:align-items :center})
(defn in-input-buttons []
- {:height 34})
+ {:flex-direction :row
+ :height 34})
(defn send-icon-color []
colors/white)
diff --git a/src/status_im/ui/screens/chat/message/audio.cljs b/src/status_im/ui/screens/chat/message/audio.cljs
new file mode 100644
index 0000000000..f43ba4bc91
--- /dev/null
+++ b/src/status_im/ui/screens/chat/message/audio.cljs
@@ -0,0 +1,231 @@
+(ns status-im.ui.screens.chat.message.audio
+ (:require-macros [status-im.utils.views :refer [defview letsubs]])
+ (:require [status-im.utils.utils :as utils]
+ [reagent.core :as reagent]
+ [goog.string :as gstring]
+ [status-im.audio.core :as audio]
+ [status-im.utils.fx :as fx]
+ [status-im.ui.screens.chat.styles.message.audio :as style]
+ [status-im.ui.components.animation :as anim]
+ [status-im.ui.components.colors :as colors]
+ [status-im.ui.components.icons.vector-icons :as icons]
+ [status-im.ui.components.react :as react]
+ [status-im.ui.components.slider :as slider]))
+
+(defn message-press-handlers [_]
+ ;;TBI save audio file?
+ )
+
+(defonce player-ref (atom nil))
+(defonce current-player-message-id (atom nil))
+(defonce current-active-state-ref-ref (atom nil))
+(defonce progress-timer (atom nil))
+
+(defn start-stop-progress-timer [{:keys [state-ref progress-ref progress-anim]} start?]
+ (when @progress-timer
+ (utils/clear-interval @progress-timer)
+ (when-not start?
+ (reset! progress-timer nil)))
+ (when start?
+ (when @progress-timer
+ (utils/clear-interval @progress-timer))
+ (reset! progress-timer (utils/set-interval
+ #(when (and @state-ref (not (:slider-seeking @state-ref)))
+ (let [ct (audio/get-player-current-time @player-ref)]
+ (reset! progress-ref ct)
+ (when ct
+ (anim/start (anim/timing progress-anim {:toValue @progress-ref
+ :duration 100
+ :easing (.-linear ^js anim/easing)
+ :useNativeDriver true})))))
+ 100))))
+
+(defn update-state [{:keys [state-ref progress-ref progress-anim message-id seek-to-ms audio-duration-ms slider-new-state-seeking? unloaded? error]}]
+ (let [player-state (audio/get-state @player-ref)
+ slider-seeking (if (some? slider-new-state-seeking?)
+ slider-new-state-seeking?
+ (:slider-seeking @state-ref))
+ general (cond
+ (some? error) :error
+ (or unloaded? (not= message-id @current-player-message-id)) :not-loaded
+ slider-seeking (:general @state-ref) ; persist player state at the time user started sliding
+ (= player-state audio/PLAYING) :playing
+ (= player-state audio/PAUSED) :paused
+ (= player-state audio/SEEKING) :seeking
+ (= player-state audio/PREPARED) :ready-to-play
+ :else :preparing)
+ new-state {:general general
+ :error-msg error
+ :duration (cond (not (#{:preparing :not-loaded :error} general))
+ (audio/get-player-duration @player-ref)
+
+ audio-duration-ms audio-duration-ms
+
+ :else (:duration @state-ref))
+ :progress-ref (or progress-ref (:progress-ref @state-ref))
+ :progress-anim (or progress-anim (:progress-anim @state-ref))
+ :slider-seeking slider-seeking
+
+ ; persist seek-to-ms while seeking or audio is not loaded
+ :seek-to-ms (when (or
+ slider-seeking
+ (#{:preparing :not-loaded :error} general))
+ (or seek-to-ms (:seek-to-ms @state-ref)))}]
+ ; update state if needed
+ (when (not= @state-ref new-state)
+ (reset! state-ref new-state))
+
+ ; update progress UI on slider release
+ (when (and (some? slider-new-state-seeking?) (not slider-new-state-seeking?) (some? seek-to-ms))
+ (reset! (:progress-ref new-state) seek-to-ms))
+
+ ; update progres anim value to follow the slider
+ (when (and slider-seeking (some? seek-to-ms))
+ (anim/set-value (:progress-anim new-state) seek-to-ms))
+
+ ; on unload, reset values
+ (when unloaded?
+ (reset! (:progress-ref new-state) 0)
+ (anim/set-value (:progress-anim new-state) 0))))
+
+(defn destroy-player [{:keys [message-id reloading?]}]
+ (when (and @player-ref (or reloading?
+ (= message-id @current-player-message-id)))
+ (audio/destroy-player @player-ref)
+ (reset! player-ref nil)
+ (when @current-active-state-ref-ref
+ (update-state {:state-ref @current-active-state-ref-ref :unloaded? true}))
+ (reset! current-player-message-id nil)
+ (reset! current-active-state-ref-ref nil)))
+
+(defonce last-seek (atom (js/Date.now)))
+
+(defn seek [{:keys [message-id] :as params} value immediate? on-success]
+ (when (and @player-ref (= message-id @current-player-message-id))
+ (let [now (js/Date.now)]
+ (when (or immediate? (> (- now @last-seek) 200))
+ (reset! last-seek (js/Date.now))
+ (audio/seek-player
+ @player-ref
+ value
+ #(do
+ (update-state params)
+ (when on-success (on-success)))
+ #(update-state (merge params {:error (:message %)}))))))
+ (update-state (merge params {:seek-to-ms value})))
+
+(defn reload-player [{:keys [message-id state-ref] :as params} base64-data on-success]
+ ;; to avoid reloading player while is initializing,
+ ;; we go ahead only if there is no player or
+ ;; if it is already prepared
+ (when (or (nil? @player-ref) (audio/can-play? @player-ref))
+ (when @player-ref
+ (destroy-player (merge params {:reloading? true})))
+ (reset! player-ref (audio/new-player
+ base64-data
+ {:autoDestroy false
+ :continuesToPlayInBackground false}
+ #(seek params 0 true nil)))
+ (audio/prepare-player
+ @player-ref
+ #(when on-success (on-success))
+ #(update-state (merge params {:error (:message %)})))
+ (reset! current-player-message-id message-id)
+ (reset! current-active-state-ref-ref state-ref)
+ (update-state params)))
+
+(defn play-pause [{:keys [message-id state-ref] :as params} audio]
+ (if (not= message-id @current-player-message-id)
+ ;; player has audio from another message, we need to reload
+ (reload-player params
+ audio
+ ;; on-success: audio is loaded, do we have an existing value to seek to?
+ #(if-some [seek-time (:seek-to-ms @state-ref)]
+ ;; check seek time against real audio duration and play
+ (let [checked-seek-time (min (audio/get-player-duration @player-ref) seek-time)]
+ (seek params
+ checked-seek-time
+ true
+ (fn [] (play-pause params audio))))
+
+ ;; nothing to seek to, play
+ (play-pause params audio)))
+
+ ;; loaded audio corresponds to current message we can play
+ (when @player-ref
+ (audio/toggle-playpause-player
+ @player-ref
+ #(do
+ (start-stop-progress-timer params true)
+ (update-state params))
+ #(do
+ (start-stop-progress-timer params false)
+ (update-state params))
+ #(update-state (merge params {:error (:message %)}))))))
+
+(defn- play-pause-button [state-ref outgoing on-press]
+ (let [color (if outgoing colors/blue colors/white-persist)]
+ (if (= (:general @state-ref) :preparing)
+ [react/view {:style (style/play-pause-container outgoing true)}
+ [react/small-loading-indicator color]]
+ [react/touchable-highlight {:on-press on-press}
+ [icons/icon (case (:general @state-ref)
+ :playing :main-icons/pause
+ :main-icons/play)
+ {:container-style (style/play-pause-container outgoing false)
+ :accessibility-label :play-pause-audio-message-button
+ :color color}]])))
+
+(fx/defn on-background
+ {:events [:audio-message/on-background]}
+ [_]
+ (when (and @current-active-state-ref-ref
+ @@current-active-state-ref-ref)
+ (update-state {:state-ref @current-active-state-ref-ref
+ :message-id @current-player-message-id}))
+ nil)
+
+(defview message-content [{:keys [audio audio-duration-ms message-id outgoing]} timestamp-view]
+ (letsubs [state (reagent/atom nil)
+ progress (reagent/atom 0)
+ progress-anim (anim/create-value 0)
+ width [:dimensions/window-width]]
+ {:component-did-mount (fn []
+ (update-state {:state-ref state
+ :audio-duration-ms audio-duration-ms
+ :message-id message-id
+ :unloaded? true
+ :progress-ref progress
+ :progress-anim progress-anim}))
+ :component-will-unmount (fn []
+ (destroy-player {:state-ref state :message-id message-id})
+ (when (= @current-player-message-id message-id)
+ (reset! current-active-state-ref-ref nil)
+ (reset! current-player-message-id nil))
+ (reset! state nil))}
+
+ (let [base-params {:state-ref state :message-id message-id :progress-ref progress :progress-anim progress-anim}]
+ (if (= (:general @state) :error)
+ [react/text {:style {:typography :main-medium
+ :margin-bottom 16}} (:error-msg @state)]
+ [react/view (style/container width)
+ [react/view style/play-pause-slider-container
+ [play-pause-button state outgoing #(play-pause base-params audio)]
+ [react/view style/slider-container
+ [slider/animated-slider (merge (style/slider outgoing)
+ {:minimum-value 0
+ :maximum-value (:duration @state)
+ :value progress-anim
+ :on-value-change #(seek base-params % false nil)
+ :on-sliding-start #(seek (merge base-params {:slider-new-state-seeking? true}) % true nil)
+ :on-sliding-complete #(seek (merge base-params {:slider-new-state-seeking? false}) % true nil)})]]]
+
+ [react/view style/times-container
+ [react/text {:style (style/timestamp outgoing)}
+ (let [time (cond
+ (or (:slider-seeking @state) (> (:seek-to-ms @state) 0)) (:seek-to-ms @state)
+ (#{:playing :paused :seeking} (:general @state)) @progress
+ :else (:duration @state))
+ s (quot time 1000)]
+ (gstring/format "%02d:%02d" (quot s 60) (mod s 60)))]
+ timestamp-view]]))))
diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs
index 7998e024d6..8fb53f5ca9 100644
--- a/src/status_im/ui/screens/chat/message/message.cljs
+++ b/src/status_im/ui/screens/chat/message/message.cljs
@@ -5,6 +5,7 @@
[status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.react :as react]
+ [status-im.ui.screens.chat.message.audio :as message.audio]
[status-im.ui.screens.chat.message.command :as message.command]
[status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.chat.sheets :as sheets]
@@ -289,6 +290,12 @@
{:content (sheets/sticker-long-press message)
:height 64}])}))
+(defn message-content-audio
+ [message]
+ [react/touchable-highlight (message.audio/message-press-handlers message)
+ [message-bubble-wrapper message
+ [message.audio/message-content message [message-timestamp message false]]]])
+
(defn chat-message [{:keys [public? content content-type] :as message}]
(if (= content-type constants/content-type-command)
[message.command/command-content message-content-wrapper message]
@@ -313,4 +320,8 @@
(not public?))
[react/touchable-highlight (image-message-press-handlers message)
[message-content-image message]]
- [unknown-content-type message])))))])))
+ (if (and (= content-type constants/content-type-audio)
+ ;; Disabling audio for public-chats
+ (not public?))
+ [message-content-audio message]
+ [unknown-content-type message]))))))])))
diff --git a/src/status_im/ui/screens/chat/styles/message/audio.cljs b/src/status_im/ui/screens/chat/styles/message/audio.cljs
new file mode 100644
index 0000000000..f30c340e7c
--- /dev/null
+++ b/src/status_im/ui/screens/chat/styles/message/audio.cljs
@@ -0,0 +1,49 @@
+(ns status-im.ui.screens.chat.styles.message.audio
+ (:require [status-im.ui.components.colors :as colors]
+ [status-im.ui.screens.chat.styles.message.message :as message.style]
+ [status-im.utils.platform :as platform]))
+
+(defn container [window-width]
+ {:width (* window-width 0.60)
+ :flex-direction :column
+ :justify-content :space-between})
+
+(def play-pause-slider-container
+ {:flex-direction :row
+ :align-items :center})
+
+(def slider-container
+ {:flex-direction :column
+ :align-items :stretch
+ :flex-grow 1})
+
+(defn slider [outgoing]
+ {:style (merge {:margin-left 12
+ :height 34}
+ (when platform/ios? {:margin-bottom 4}))
+ :thumb-tint-color (if outgoing
+ colors/white
+ colors/blue)
+ :minimum-track-tint-color (if outgoing
+ colors/white
+ colors/blue)
+ :maximum-track-tint-color (if outgoing
+ colors/white-transparent
+ colors/gray-transparent-40)})
+
+(defn play-pause-container [outgoing? loading?]
+ {:background-color (if outgoing? colors/white-persist colors/blue)
+ :width 28
+ :height 28
+ :padding (if loading? 4 2)
+ :border-radius 15})
+
+(def times-container
+ {:flex-direction :row
+ :justify-content :space-between})
+
+(defn timestamp [outgoing]
+ (merge (message.style/message-timestamp-text
+ false
+ outgoing
+ false) {:margin-left 40}))
\ No newline at end of file
diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs
index e6e5164f43..24950488ac 100644
--- a/src/status_im/ui/screens/chat/views.cljs
+++ b/src/status_im/ui/screens/chat/views.cljs
@@ -11,6 +11,7 @@
[status-im.ui.screens.chat.sheets :as sheets]
[quo.animated :as animated]
[quo.react-native :as rn]
+ [status-im.ui.screens.chat.audio-message.views :as audio-message]
[status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.stickers.views :as stickers]
[status-im.ui.screens.chat.styles.main :as style]
@@ -177,6 +178,8 @@
[extensions/extensions-view]
:images
[image/image-view]
+ :audio
+ [audio-message/audio-message-view]
nil))
(defn chat []
diff --git a/src/status_im/ui/screens/home/views/inner_item.cljs b/src/status_im/ui/screens/home/views/inner_item.cljs
index 211053580d..611c03ea54 100644
--- a/src/status_im/ui/screens/home/views/inner_item.cljs
+++ b/src/status_im/ui/screens/home/views/inner_item.cljs
@@ -87,6 +87,11 @@
:accessibility-label :no-messages-text}
(i18n/label :t/image)]
+ (= constants/content-type-audio content-type)
+ [react/text {:style styles/last-message-text
+ :accessibility-label :no-messages-text}
+ (i18n/label :t/audio)]
+
(string/blank? (:text content))
[react/text {:style styles/last-message-text}
""]
diff --git a/src/status_im/utils/fs.cljs b/src/status_im/utils/fs.cljs
index ca7a3fdceb..df2a951452 100644
--- a/src/status_im/utils/fs.cljs
+++ b/src/status_im/utils/fs.cljs
@@ -4,6 +4,11 @@
(defn move-file [src dst]
(.moveFile react-native-fs src dst))
+(defn stat [path on-stat on-error]
+ (-> (.stat react-native-fs path)
+ (.then on-stat)
+ (.catch on-error)))
+
(defn read-file [path encoding on-read on-error]
(-> (.readFile react-native-fs path encoding)
(.then on-read)
diff --git a/status-go-version.json b/status-go-version.json
index 70668810fc..190af1b54b 100644
--- a/status-go-version.json
+++ b/status-go-version.json
@@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead",
"owner": "status-im",
"repo": "status-go",
- "version": "v0.55.2",
- "commit-sha1": "0b3cdf7362bbdf9ba7fc11da803105f9417dfbac",
- "src-sha256": "1vq3z150p0fbwjc1mqmi8lz4vg28dzqhlpsn7kar8j5z4rx5z5hn"
+ "version": "v0.56.1",
+ "commit-sha1": "4574ab4c22ee6b662a7f87c39d0f714998c567dc",
+ "src-sha256": "0jd684lv7x93gxgvvhsv4ihxr5sw2rck2divsxfiql5g2c1v4alg"
}
diff --git a/translations/en.json b/translations/en.json
index 048a8acd14..73ec0de129 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -1169,5 +1169,13 @@
"private-notifications": "Private notifications",
"private-notifications-descr": "Status will notify you about new messages. You can edit your notification preferences later in settings.",
"maybe-later": "Maybe later",
- "join": "Join"
+ "join": "Join",
+ "audio-recorder-error": "Recorder error",
+ "audio-recorder": "Recorder",
+ "audio-recorder-max-ms-reached": "Maximum recording time reached",
+ "audio-recorder-permissions-error": "You have to give permission to send audio messages",
+ "audio": "Audio",
+ "update-to-see-image": "Update to latest version to see a nice image here!",
+ "update-to-listen-audio": "Update to latest version to listen to an audio message here!",
+ "update-to-see-sticker": "Update to latest version to see a nice sticker here!"
}
diff --git a/yarn.lock b/yarn.lock
index 04fc3a7979..7f4a5160c2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1207,6 +1207,14 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
+"@react-native-community/audio-toolkit@git+https://github.com/tbenr/react-native-audio-toolkit.git#v2.0.3-status-v6":
+ version "2.0.3"
+ resolved "git+https://github.com/tbenr/react-native-audio-toolkit.git#7ae9055cf6169b30f5089bda7bfcfc1c40a715e5"
+ dependencies:
+ async "^2.6.3"
+ eventemitter3 "^1.2.0"
+ lodash "^4.17.15"
+
"@react-native-community/cameraroll@^1.6.1":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cameraroll/-/cameraroll-1.6.2.tgz#a4dedcf8ba7bc938f805dd07dd43a275edb1f411"
@@ -1329,6 +1337,11 @@
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-4.7.0.tgz#7482d36836cac69d0a0ae25581f65bc472639930"
integrity sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==
+"@react-native-community/slider@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.0.tgz#ffbf78689fc0572fb5c1e2ccb61b2ef074d3dcd2"
+ integrity sha512-deNc3JHBHz24YN+0DTAocXfrYFIFc1DvsIriMJSsJlR/MvsLzoq2+qwaEN+0/LJ37pstv85wZWY0pNugk4e41g==
+
"@react-navigation/bottom-tabs@^5.7.0":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.7.1.tgz#d6b4e61676f8b4ab11864e792c819c56fde32a44"
@@ -1822,7 +1835,7 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
-async@^2.4.0:
+async@^2.4.0, async@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@@ -3251,6 +3264,11 @@ event-target-shim@^5.0.0, event-target-shim@^5.0.1:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+eventemitter3@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
+ integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=
+
eventemitter3@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
@@ -6554,6 +6572,11 @@ react-native-navigation-bar-color@^2.0.1:
resolved "https://registry.yarnpkg.com/react-native-navigation-bar-color/-/react-native-navigation-bar-color-2.0.1.tgz#ee2be25cc37105f7da355717b0a9a32c9c059ae6"
integrity sha512-1kE/oxWt+HYjRxdZdvke9tJ365xaee5n3+euOQA1En8zQuSbOxiE4SYEGM7TeaWnmLJ0l37mRnPHaB2H4mGh0A==
+react-native-permissions@^2.1.5:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-2.1.5.tgz#6cfc4f1ab1590f4952299b7cdc9698525ad540e0"
+ integrity sha512-b9KO/4UEV9qddl+kcSybmdk8nlAifclSDBR2rSvc5KZM06vIaJWJNIzK2ZwPXqDQ5yD3CJLuKTRj7Fz+jM9qyQ==
+
react-native-reanimated@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-1.8.0.tgz#0b5719b20c1fed9aaf8afd9a12e21c9bd46ee428"