From 19b3547ab15002c0471baef61fccda89a57ad974 Mon Sep 17 00:00:00 2001 From: tbenr Date: Thu, 23 Jul 2020 20:06:48 +0200 Subject: [PATCH] audio messages --- android/app/src/main/AndroidManifest.xml | 2 + ios/Podfile | 4 + ios/Podfile.lock | 49 ++- ios/StatusIm/Info.plist | 2 + ios/StatusImPR/Info.plist | 2 + .../status/ethereum/module/StatusModule.java | 28 ++ .../ios/RCTStatus/RCTStatus.m | 14 + nix/deps/gradle/deps.json | 16 +- nix/deps/gradle/deps.urls | 4 +- nix/deps/gradle/proj.list | 3 + package.json | 3 + resources/images/icons/pause@2x.png | Bin 0 -> 216 bytes resources/images/icons/pause@3x.png | Bin 0 -> 420 bytes resources/images/icons/play@2x.png | Bin 0 -> 288 bytes resources/images/icons/play@3x.png | Bin 0 -> 400 bytes resources/images/icons/speech@2x.png | Bin 0 -> 502 bytes resources/images/icons/speech@3x.png | Bin 0 -> 762 bytes src/status_im/audio/core.cljs | 129 ++++++++ src/status_im/chat/models/input.cljs | 9 + src/status_im/constants.cljs | 1 + src/status_im/data_store/messages.cljs | 3 +- src/status_im/events.cljs | 8 +- src/status_im/native_module/core.cljs | 8 + src/status_im/transport/message/protocol.cljs | 4 + src/status_im/ui/components/colors.cljs | 1 + src/status_im/ui/components/permissions.cljs | 27 +- src/status_im/ui/components/slider.cljs | 9 + .../ui/screens/chat/audio_message/styles.cljs | 48 +++ .../ui/screens/chat/audio_message/views.cljs | 292 ++++++++++++++++++ .../ui/screens/chat/components/input.cljs | 21 +- .../ui/screens/chat/components/style.cljs | 3 +- .../ui/screens/chat/message/audio.cljs | 231 ++++++++++++++ .../ui/screens/chat/message/message.cljs | 13 +- .../ui/screens/chat/styles/message/audio.cljs | 49 +++ src/status_im/ui/screens/chat/views.cljs | 3 + .../ui/screens/home/views/inner_item.cljs | 5 + src/status_im/utils/fs.cljs | 5 + status-go-version.json | 6 +- translations/en.json | 7 +- yarn.lock | 25 +- 40 files changed, 992 insertions(+), 42 deletions(-) create mode 100755 resources/images/icons/pause@2x.png create mode 100755 resources/images/icons/pause@3x.png create mode 100755 resources/images/icons/play@2x.png create mode 100755 resources/images/icons/play@3x.png create mode 100644 resources/images/icons/speech@2x.png create mode 100644 resources/images/icons/speech@3x.png create mode 100644 src/status_im/audio/core.cljs create mode 100644 src/status_im/ui/components/slider.cljs create mode 100644 src/status_im/ui/screens/chat/audio_message/styles.cljs create mode 100644 src/status_im/ui/screens/chat/audio_message/views.cljs create mode 100644 src/status_im/ui/screens/chat/message/audio.cljs create mode 100644 src/status_im/ui/screens/chat/styles/message/audio.cljs diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cc5f6f072fb7..4254941dd844 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 088c84cf4f0b..312878c51b6e 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 a724456a75c4..b5b6f4af2581 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 573ee89cb672..559a216a7caa 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 02441fc97d30..05ec5f0ef1df 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 78e9ad3c82f3..2a67022f8fee 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 @@ -1318,5 +1318,33 @@ public void run() { 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 c9299e69ac6f..935591a2d3a0 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -726,6 +726,20 @@ - (void) migrateKeystore:(NSString *)accountData [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 5476a1d9db9e..2e403a9e20f3 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 abd783de15ac..3140123e6781 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 ec409cb12c4c..04197555e2ed 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 a56e8235a857..cda4775e5bfb 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 0000000000000000000000000000000000000000..83ceee86e324a4cfb2e6ba564f35d591ea4514bc GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D`JOJ0Ar-gYPCLlkpupq$pQGnN z+Kxu$8@vU=?g=I~4jqAtYdbp?{}*noY`kBQ$>?`0rLn`9lPCMAwY{28v{MKSZo5+h# z|M&0m6_8fma>!QU>w|Ks-dXMWvgQ13i4rGxo{K3R@7MXeVc*Bc8~OHanG*O~vU}O1 zqtYL$t!+x?Rptu(sr0y_CA@A@>iYTZR$4Cm;_ffqV*7OMQ>_n;>8l(o1rj@BJNH_w z(B@ptspq<~<5-zY?#IwY7J4yVRv-87d!pO*c!At<;klb@{^TkN&phFxW;C($-T!8u zB+Cod`=Y))61ShOl%brYW!U|AnVC|6Wp>812j@4dEjQsk75Vs)*2(ISFxd%XD?dj_0vd$@? F2>`I8mU93A literal 0 HcmV?d00001 diff --git a/resources/images/icons/play@2x.png b/resources/images/icons/play@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..58785d143bd2f5ea647d00f6a4b980635d3963a1 GIT binary patch literal 288 zcmV+*0pI?KP)^Dgg(ZgU!KkKpij#NT`HLKmrn28kV1?8$4|Hzid5TNxwUeKw2oioxoH4m23Rn`vH|PCErLLL7(O8Bj7f5wGyp(@yX2sio&!{JlmL2; zR{%N3djK)V9w5m{ju}@C0ASEbj`&%QW5D{cwj5Rikkm;VDgcB2`JUGRfj!Rv0000)?4COY3_wv7 zMNt%`X*wKnhKEDng3f>&?yqrXOZAaX7&q*hLKmS!@AT}mh*GppPXPy%4F>XCXqTZ$g1x(#ddz!g;`nPNz~Y zFHdM|8!oiDggVTkjYS3C zJ?tnJKaYX=qv*xrSMi5B6-wI>f2FByVj;(G6JJ=}lTDL!VdXZj^yadQ-{+&&l3aQk uP>B!hj@Kr><(M(UvD+w$q9}@@{D}t(-sFoB%nktn0000i zgD?<F=h z3|E2$3f;gCcYF^$UkwHnxCWGa_?N^#0|RL7YY%@-{3|ekmV^(K$f?1TMsW_VCxlK4T^!68Io?>fyg5_Ur^gpwaDZ?j@1- zd=;2r+hQ7+ZL78WFL8F+^I_3Sw`~4OX|VzIS7J;V7zR&KXl`kAMJhCbYY5>`2vZ6P zBAiA$G^O!v^oHr|-5B-EasEw#@pm9!G=|4&julbpzJ1c;agXk!R_kwnOEC<*-Q)@@ z<9lqfKUNinCH0qV-De0e>mi<1TI?*opedCn3_H`a)2Gq9(GQqYV1*63@O^Y sTq$EC3B+ctO!~4ca zwSU5#1G^k233sIK`oZQk)5xmvkzVX1;Rw6Nr|0JO$Of6QPK@FbF%~6_nR{lS~$2E&2)471UZ*(c1l0-%;R$TK83;( z%Ur8lJc5Gy!IjI?VIJBTQ|?%^jEC4|I^7iwidseTLiA?YroxX_2Hh#?sL0; z%~MdUOZcz+_3+Z8yPK*ePV-=K++lIm@KxFa7S09M{r?lg86W#(A3HgbVVB6Co5Ef8 z-Bs~v>o3|*kNa(ZD6%5g@%#~i6W@jZ3!FQZ&0VF|=8(?IYP7(CP2@xalVpGbGgpd3 zBag-eMugxIfsChH?<>#YUcc9k=?CZL>XygPmreGVaknD(EPv~=n4_0tg>TK$^JbPlnYDLKK|-%T(B?heu{U`k7YCCx&s+kZGN@w&FP6Af9Lj0-Iq6m zvuoBKw#of3#ABP}PwhXiR53U4`t+0+{XDyuzuA1_>8mnDTLmt^<4a^Vztb?Nn03zL z_J&2L*KOHbQTL*rM~+vJPnPBDO_`&gGL)|^`SZ54yKuRMVeQeRrf==my$8e2z5DPe zC3H%;hRom8(}s45_SKdZMJ4@v*L+YuK2zC3Z;gf9j3s_In-01Eaou6y?INa@X1Ahi zLBXC^@p>0(!hZECxi7JMCN8uxsbzun6#M9+3F$^}xThpCY}k~A9;`gB3rbJ#3kjs 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 canPlay? [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 e3d553a34efb..0c8efc1a86a7 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -108,6 +108,15 @@ :image-path (string/replace image-path #"file://" "") :text "Update to latest version to see a nice image here!"}))))) +(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 "Update to latest version to listen to an audio message here!"}))) + (fx/defn send-sticker-message [cofx {:keys [hash pack]} current-chat-id] (when-not (string/blank? hash) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index bbe3382f9d08..73b38abf70c7 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 c4a9ba5f4103..5c2a222dfc96 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 a10a3611e4ad..409ea241f997 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -559,6 +559,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 _] @@ -1237,7 +1242,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 53d978e2dd04..537e3c6bbfe8 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -357,3 +357,11 @@ [mnemonic callback] (log/debug "[native-module] validate-mnemonic") (.validateMnemonic ^js (status) mnemonic 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))) \ No newline at end of file diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index eb5a5c697750..1dddfb45882a 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 fb97027b6f9e..d1c53bb73f1b 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 cb85e584018c..26132ea364ce 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 000000000000..90ee45a35c19 --- /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 000000000000..881c175b04c3 --- /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 000000000000..422531448eb3 --- /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 5cafc39f8a55..1a6afdbabfc7 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 0fb7da4542fa..1cba33a325d4 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 000000000000..f66d4cbea096 --- /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/canPlay? @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 7998e024d643..8fb53f5ca903 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 000000000000..f30c340e7c3e --- /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 e6e5164f438a..24950488ac97 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 211053580d5b..611c03ea5446 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 ca7a3fdceb31..df2a951452d3 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 40d3298312bd..cd5f115ea9b7 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.1", - "commit-sha1": "6d5a93287b2c1b4d4d5d1178a3ecec870bf18b9e", - "src-sha256": "1wly0km9bxxv1wwj6jchqh4d4x2m86fxrdqixjzldy70vl6qbyqa" + "version": "feature/audio-messages", + "commit-sha1": "b9a7e6ff2356c80a1b541b0dff42872cd79d0592", + "src-sha256": "0jkbaqywxgpp0zz3jvprknfpxbdqkjhhp9nz912kng5qmq10fgqm" } diff --git a/translations/en.json b/translations/en.json index 4834a0edad81..514202c50551 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1164,5 +1164,10 @@ "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" } diff --git a/yarn.lock b/yarn.lock index 04fc3a797947..7f4a5160c2f4 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"