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"