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.lock b/ios/Podfile.lock index 23b5bb556f69..72ba2aa8d398 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -255,6 +255,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 +320,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): @@ -354,8 +358,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) @@ -406,6 +410,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 +424,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`)" @@ -437,11 +443,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 +455,8 @@ SPEC REPOS: - Flipper-RSocket - FlipperKit - OpenSSL-Universal + - SQLCipher + - SSZipArchive - TOCropViewController - YogaKit @@ -500,6 +505,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 +531,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: @@ -587,6 +596,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 +609,7 @@ SPEC CHECKSUMS: React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 + ReactNativeAudioToolkit: de9610f323e855ac6574be8c99621f3d57c5df06 ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949 RNCClipboard: 8148e21ac347c51fd6cd4b683389094c216bb543 RNCMaskedView: 71fc32d971f03b7f03d6ab6b86b730c4ee64f5b6 @@ -612,12 +623,12 @@ SPEC CHECKSUMS: RNScreens: ac02d0e4529f08ced69f5580d416f968a6ec3a1d RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 - SSZipArchive: fa16b8cc4cdeceb698e5e5d9f67e9558532fbf23 - TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729 + SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9 + TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6 TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4 Yoga: 3ebccbdd559724312790e7742142d062476b698e YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: f66349c5bfb9c21ac968307ea5a2d6c2dd4091ed -COCOAPODS: 1.9.3 +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/proj.list b/nix/deps/gradle/proj.list index 8b3dde7b6b65..1cab0b71a9ae 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 diff --git a/package.json b/package.json index aab8d91c84e3..3c09c0a8af31 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.1.1", "@react-navigation/native": "^5.2.3", "@react-navigation/stack": "^5.1.1", diff --git a/resources/images/icons/pause@2x.png b/resources/images/icons/pause@2x.png new file mode 100755 index 000000000000..83ceee86e324 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 000000000000..8691dd1676d9 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 000000000000..58785d143bd2 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 000000000000..6d05bb410ac7 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 000000000000..45f36003b3bf 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 000000000000..93c7c09ed011 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 000000000000..0a65c3e1304f --- /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 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 56eb62a89c6c..0e0dde4f4a28 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -126,6 +126,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 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 010f516ccaf4..616bb19e6444 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 _] 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..74ce8bc21933 100644 --- a/src/status_im/ui/components/permissions.cljs +++ b/src/status_im/ui/components/permissions.cljs @@ -5,7 +5,8 @@ (def permissions-map {:read-external-storage "android.permission.READ_EXTERNAL_STORAGE" :write-external-storage "android.permission.WRITE_EXTERNAL_STORAGE" - :camera "android.permission.CAMERA"}) + :camera "android.permission.CAMERA" + :record-audio "android.permission.RECORD_AUDIO"}) (defn all-granted? [permissions] (let [permission-vals (distinct (vals permissions))] 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..0289d7ccec1e --- /dev/null +++ b/src/status_im/ui/screens/chat/audio_message/styles.cljs @@ -0,0 +1,49 @@ +(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 + :margin-vertical 40}) + +(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..90c2d2c821e2 --- /dev/null +++ b/src/status_im/ui/screens/chat/audio_message/views.cljs @@ -0,0 +1,318 @@ +(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.core :as quo] + [status-im.native-module.core :as status] + [status-im.ui.screens.chat.input.send-button :as send-button] + [status-im.ui.screens.chat.styles.input.send-button :as send-button.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])) + +;; 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 #())) + +;; 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} + @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 + - :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)) + +;; permission management +(defn- request-record-audio-permissions [] + (re-frame/dispatch + [:request-permissions + {:permissions [:record-audio] + :on-allowed + #(re-frame/dispatch [:chat.ui/set-chat-ui-props + {:input-bottom-sheet :audio-message}]) + :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 show-panel-anim + [bottom-anim-value alpha-value] + (anim/start + (anim/parallel + [(anim/spring bottom-anim-value {:toValue 0 + :useNativeDriver true}) + (anim/timing alpha-value {:toValue 1 + :duration 500 + :useNativeDriver true})]))) + +(defn input-button [audio-message-showing?] + [quo/button + {:on-press (fn [_] + (if audio-message-showing? + (re-frame/dispatch [:chat.ui/set-chat-ui-props + {:input-bottom-sheet nil}]) + (request-record-audio-permissions)) + (js/setTimeout #(react/dismiss-keyboard!) 100)) + :accessibility-label :show-audio-message-icon + :type :icon + :theme (if audio-message-showing? :main :disabled)} + :main-icons/speech]) + +;; 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] + [quo/button {:type :scale + :disabled disabled? + :on-press on-press} + [icons/icon :main-icons/close + {:container-style (merge send-button.style/send-message-container {:background-color colors/gray}) + :accessibility-label :cancel-message-button + :color colors/white-persist}]]) + +(defview audio-message-view [] + (letsubs [panel-height [:chats/chat-panel-height] + bottom-anim-value (anim/create-value @panel-height) + alpha-value (anim/create-value 0) + 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) + (show-panel-anim bottom-anim-value alpha-value) + (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)))) + (reload-recorder)) + :component-will-unmount (fn [] + (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))} + (let [base-params {:rec-button-anim-value rec-button-anim-value + :ctrl-buttons-anim-value ctrl-buttons-anim-value + :timer timer}] + [react/animated-view {:style {:background-color colors/white + :height panel-height + :transform [{:translateY bottom-anim-value}] + :opacity alpha-value}} + + [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}} + [send-button/send-button-view false (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/input/input.cljs b/src/status_im/ui/screens/chat/input/input.cljs index 8c8e7ccf065a..f2fa48106606 100644 --- a/src/status_im/ui/screens/chat/input/input.cljs +++ b/src/status_im/ui/screens/chat/input/input.cljs @@ -12,6 +12,7 @@ [status-im.ui.components.react :as react] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.utils.config :as config] + [status-im.ui.screens.chat.audio-message.views :as audio-message] [status-im.ui.screens.chat.image.views :as image] [status-im.ui.screens.chat.stickers.views :as stickers] [status-im.ui.screens.chat.extensions.views :as extensions])) @@ -99,4 +100,6 @@ [extensions/button (= :extensions input-bottom-sheet)]) (when-not input-text-empty? [send-button/send-button-view input-text-empty? - #(re-frame/dispatch [:chat.ui/send-current-message])])]]))) + #(re-frame/dispatch [:chat.ui/send-current-message])]) + (when (and input-text-empty? (not reply-message) (not public?)) + [audio-message/input-button (= :audio-message input-bottom-sheet)])]]))) 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..0a5d97f36b67 --- /dev/null +++ b/src/status_im/ui/screens/chat/message/audio.cljs @@ -0,0 +1,206 @@ +(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.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 [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 + #(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 audio-duration-ms slider-seeking unloaded? error]}] + (let [player-state (audio/get-state @player-ref) + general (cond + (some? error) :error + (or unloaded? (not= message-id @current-player-message-id)) :not-loaded + (= player-state audio/PLAYING) :playing + (= player-state audio/PAUSED) :paused + (= player-state audio/SEEKING) :seeking + (= player-state audio/PREPARED) :ready-to-play + :else :preparing) + slider-seeking' (if (some? slider-seeking) + slider-seeking + (:slider-seeking @state-ref)) + 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' + :seek (when (or + slider-seeking' + (#{:preparing :not-loaded :error} general)) + (or seek (:seek @state-ref)))}] + (when (not= @state-ref new-state) + (reset! state-ref new-state)) + (when (and (not= general :playing) (not slider-seeking') (some? seek)) + (reset! progress-ref seek)) + (when (and slider-seeking' (some? seek)) + (anim/set-value progress-anim seek)) + (when unloaded? + (reset! (:progress-ref @state-ref) 0) + (anim/set-value (:progress-anim @state-ref) 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 + #(when on-success (on-success)) + #(update-state (merge params {:error (:message %)})))))) + (update-state (merge params {:seek value}))) + +(defn reload-player-and-play [{: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} + #(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-and-play params + audio + ;; on-success: audio is loaded, do we have an existing value to seek to? + #(if-some [seek-time (:seek @state-ref)] + ;; seek and play on-success + (seek params + 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}]]))) + +(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 [] + (reset! state nil) + (when (= @current-player-message-id message-id) + (reset! current-active-state-ref-ref nil) + (reset! current-player-message-id nil)) + (destroy-player {:state-ref state :message-id message-id}))} + + (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-seeking true}) % true nil) + :on-sliding-complete #(seek (merge base-params {:slider-seeking false}) % true nil)})]]] + + [react/view style/times-container + [react/text {:style (style/timestamp outgoing)} + (let [time (cond + (or (:slider-seeking @state) (> (:seek @state) 0)) (:seek @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 85c9a4d70f3b..9e91aeab6491 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] @@ -292,6 +293,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] @@ -316,4 +323,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 a60d018dba59..56878b9970c9 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -9,6 +9,7 @@ [status-im.ui.components.react :as react] [status-im.ui.screens.chat.sheets :as sheets] [status-im.ui.screens.chat.input.input :as input] + [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] @@ -174,6 +175,8 @@ [extensions/extensions-view] :images [image/image-view] + :audio-message + [audio-message/audio-message-view] [empty-bottom-sheet]))) (defview chat [] 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 1c235e75af27..21a66631a443 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1162,5 +1162,9 @@ "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" } diff --git a/yarn.lock b/yarn.lock index 8c958422bbfe..5fe6044d1298 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.1.1": version "5.2.7" resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.2.7.tgz#6f3eca9ba323cd9e80dd4ceba1f1c8e84955091f" @@ -1821,7 +1834,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== @@ -3270,6 +3283,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"