audio messages

Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
tbenr 2020-07-28 13:06:58 +02:00 committed by Andrea Maria Piana
parent 2cc8c9822f
commit 86700f2b5c
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
40 changed files with 998 additions and 44 deletions

View File

@ -21,6 +21,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="com.android.vending.BILLING" />
<!-- these permissions should be removed -->

View File

@ -81,6 +81,10 @@ abstract_target 'Status' do
pod 'SQLCipher', '~>3.0'
pod 'SSZipArchive'
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone.podspec"
pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
target 'StatusIm' do
target 'StatusImTests' do
inherit! :complete

View File

@ -70,6 +70,10 @@ PODS:
- OpenSSL-Universal (1.0.2.19):
- OpenSSL-Universal/Static (= 1.0.2.19)
- OpenSSL-Universal/Static (1.0.2.19)
- Permission-Camera (2.1.5):
- RNPermissions
- Permission-Microphone (2.1.5):
- RNPermissions
- RCTRequired (0.62.2)
- RCTTypeSafety (0.62.2):
- FBLazyVector (= 0.62.2)
@ -255,6 +259,8 @@ PODS:
- React
- react-native-shake (3.4.0):
- React
- react-native-slider (3.0.0):
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webview (10.3.1):
@ -318,6 +324,8 @@ PODS:
- React-cxxreact (= 0.62.2)
- React-jsi (= 0.62.2)
- ReactCommon/callinvoker (= 0.62.2)
- ReactNativeAudioToolkit (2.0.3):
- React
- ReactNativeDarkMode (0.2.2):
- React
- RNCClipboard (1.2.2):
@ -341,6 +349,8 @@ PODS:
- React
- RNLanguages (3.0.2):
- React
- RNPermissions (2.1.5):
- React
- RNReactNativeHapticFeedback (1.9.0):
- React
- RNReanimated (1.8.0):
@ -354,8 +364,8 @@ PODS:
- SQLCipher/common (3.4.2)
- SQLCipher/standard (3.4.2):
- SQLCipher/common
- SSZipArchive (2.2.2)
- TOCropViewController (2.5.2)
- SSZipArchive (2.2.3)
- TOCropViewController (2.5.3)
- TouchID (4.4.1):
- React
- Yoga (1.14.0)
@ -387,6 +397,8 @@ DEPENDENCIES:
- FlipperKit/SKIOSNetworkPlugin (~> 0.37.0)
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera.podspec`)
- Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
@ -406,6 +418,7 @@ DEPENDENCIES:
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-shake (from `../node_modules/react-native-shake`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@ -419,6 +432,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "ReactNativeAudioToolkit (from `../node_modules/@react-native-community/audio-toolkit`)"
- ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`)
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
@ -427,6 +441,7 @@ DEPENDENCIES:
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLanguages (from `../node_modules/react-native-languages`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
@ -437,11 +452,8 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- boost-for-react-native
- SQLCipher
- SSZipArchive
trunk:
- boost-for-react-native
- CocoaAsyncSocket
- CocoaLibEvent
- Flipper
@ -452,6 +464,8 @@ SPEC REPOS:
- Flipper-RSocket
- FlipperKit
- OpenSSL-Universal
- SQLCipher
- SSZipArchive
- TOCropViewController
- YogaKit
@ -466,6 +480,10 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec"
glog:
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
Permission-Camera:
:path: "../node_modules/react-native-permissions/ios/Camera.podspec"
Permission-Microphone:
:path: "../node_modules/react-native-permissions/ios/Microphone.podspec"
RCTRequired:
:path: "../node_modules/react-native/Libraries/RCTRequired"
RCTTypeSafety:
@ -500,6 +518,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-shake:
:path: "../node_modules/react-native-shake"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-splash-screen:
:path: "../node_modules/react-native-splash-screen"
react-native-webview:
@ -524,6 +544,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/Vibration"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
ReactNativeAudioToolkit:
:path: "../node_modules/@react-native-community/audio-toolkit"
ReactNativeDarkMode:
:path: "../node_modules/react-native-dark-mode"
RNCClipboard:
@ -540,6 +562,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keychain"
RNLanguages:
:path: "../node_modules/react-native-languages"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
@ -568,8 +592,10 @@ SPEC CHECKSUMS:
Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7
FlipperKit: afd4259ef9eadeeb2d30250b37d95cb3b6b97a69
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
glog: 682164e7ac67e41afd8f7b6a37a96d04caf61cc0
OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355
Permission-Camera: afad27bf90337684d4a86f3825112d648c8c4d3b
Permission-Microphone: 0ffabc3fe1c75cfb260525ee3f529383c9f4368c
RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035
RCTTypeSafety: 93006131180074cffa227a1075802c89a49dd4ce
React: 29a8b1a02bd764fb7644ef04019270849b9a7ac3
@ -587,6 +613,7 @@ SPEC CHECKSUMS:
react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9
react-native-safe-area-context: 60f654e00b6cc416573f6d5dbfce3839958eb57a
react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c
react-native-slider: 12bd76d3d568c9c5500825db54123d44b48e4ad4
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webview: 40bbeb6d011226f34cb83f845aeb0fdf515cfc5f
React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c
@ -599,6 +626,7 @@ SPEC CHECKSUMS:
React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d
React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256
ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3
ReactNativeAudioToolkit: de9610f323e855ac6574be8c99621f3d57c5df06
ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949
RNCClipboard: 8148e21ac347c51fd6cd4b683389094c216bb543
RNCMaskedView: 71fc32d971f03b7f03d6ab6b86b730c4ee64f5b6
@ -607,17 +635,18 @@ SPEC CHECKSUMS:
RNImageCropPicker: 38865ab4af1b0b2146ad66061196bc0184946855
RNKeychain: 216f37338fcb9e5c3a2530f1e3295f737a690cb1
RNLanguages: 962e562af0d34ab1958d89bcfdb64fafc37c513e
RNPermissions: ad71dd4f767ec254f2cd57592fbee02afee75467
RNReactNativeHapticFeedback: 2566b468cc8d0e7bb2f84b23adc0f4614594d071
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
RNScreens: ac02d0e4529f08ced69f5580d416f968a6ec3a1d
RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f
SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990
SSZipArchive: fa16b8cc4cdeceb698e5e5d9f67e9558532fbf23
TOCropViewController: e9da34f484aedd4e5d5a8ab230ba217cfe16c729
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6
TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4
Yoga: 3ebccbdd559724312790e7742142d062476b698e
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: f66349c5bfb9c21ac968307ea5a2d6c2dd4091ed
PODFILE CHECKSUM: 5faa578ff5cb7a30abc18b9d620df288750a72fe
COCOAPODS: 1.9.1

View File

@ -83,6 +83,8 @@
<string>Location access is required for some DApps to function properly.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photos access is required to give you the ability to send images.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for sending audio messages.</string>
<key>UIAppFonts</key>
<array>
<string>Inter-Bold.otf</string>

View File

@ -83,6 +83,8 @@
<string>Location access is required for some DApps to function properly.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photos access is required to give you the ability to send images.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for sending audio messages.</string>
<key>UIAppFonts</key>
<array>
<string>Inter-Bold.otf</string>

View File

@ -1346,5 +1346,33 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r);
}
@ReactMethod
public void activateKeepAwake() {
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
@ReactMethod
public void deactivateKeepAwake() {
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
}

View File

@ -749,6 +749,20 @@ RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue)
[userDefaults synchronize];
}
RCT_EXPORT_METHOD(activateKeepAwake)
{
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
});
}
RCT_EXPORT_METHOD(deactivateKeepAwake)
{
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
});
}
//// deviceinfo
- (bool) is24Hour

View File

@ -5794,16 +5794,16 @@
},
{
"path": "com/google/protobuf/protobuf-java/3.12.2/protobuf-java-3.12.2",
"path": "com/google/protobuf/protobuf-java/4.0.0-rc-2/protobuf-java-4.0.0-rc-2",
"host": "https://repo.maven.apache.org/maven2",
"type": "jar",
"pom": {
"sha1": "da3bb9dce2a722503f0d3e4f3b5eea93f737ba48",
"sha256": "1ijicnm83gq33px9c5r4man0n2zw6axrxsqw0jnm1miixyjz08cv"
"sha1": "8fab0fe0746b1d370663a3db7dd483c2c4d34a6f",
"sha256": "17agfgbdfxdk304fgzk0iwsf7ggwqrwb44b3nnjc0xjdgqlxmzxb"
},
"jar": {
"sha1": "f0029524002295154c6b546d4e6bd1cfc5081874",
"sha256": "186a1lgmcfr3iml7bfxk9gnbsafyxmriqfyglvkwa85d7gwvx1dk"
"sha1": "ae590f34383d2d6fde2b3b8e2ffb668e64579099",
"sha256": "0al23s9s97sxlrrndb91cnra07fdchbcgvhcacyd6yjzysz55hiw"
}
},
@ -5828,12 +5828,12 @@
},
{
"path": "com/google/protobuf/protobuf-parent/3.12.2/protobuf-parent-3.12.2",
"path": "com/google/protobuf/protobuf-parent/4.0.0-rc-2/protobuf-parent-4.0.0-rc-2",
"host": "https://repo.maven.apache.org/maven2",
"type": "pom",
"pom": {
"sha1": "f5b5d4cb5e6d512f2fac5d2b08e599644a53c087",
"sha256": "0md51xi5af8yi9l1dpqbmysrvlll05zpvmalag6p4f6fykwcry05"
"sha1": "c5391c7917ca1197cbbda4efc6454a87e7e7107e",
"sha256": "0bvynr52xjyd42n23ih635h2cmyillfbac0yjymhlp505f0p1lp0"
}
},

View File

@ -417,10 +417,10 @@ https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs/1.1/jimfs-1.1.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.4.0/protobuf-java-util-3.4.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.0.0/protobuf-java-3.0.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.4.0/protobuf-java-3.4.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.12.2/protobuf-java-3.12.2.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/4.0.0-rc-2/protobuf-java-4.0.0-rc-2.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.0.0/protobuf-parent-3.0.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.4.0/protobuf-parent-3.4.0.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.12.2/protobuf-parent-3.12.2.pom
https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/4.0.0-rc-2/protobuf-parent-4.0.0-rc-2.pom
https://repo.maven.apache.org/maven2/com/google/truth/truth-parent/1.0.1/truth-parent-1.0.1.pom
https://repo.maven.apache.org/maven2/com/google/truth/truth/1.0.1/truth-1.0.1.pom
https://repo.maven.apache.org/maven2/com/google/zxing/core/3.3.3/core-3.3.3.pom

View File

@ -1,10 +1,12 @@
app
react-native-background-timer
react-native-camera
react-native-community_audio-toolkit
react-native-community_cameraroll
react-native-community_clipboard
react-native-community_masked-view
react-native-community_netinfo
react-native-community_slider
react-native-config
react-native-dark-mode
react-native-dialogs
@ -18,6 +20,7 @@ react-native-languages
react-native-mail
react-native-navigation-bar-color
react-native-nfc-manager
react-native-permissions
react-native-reanimated
react-native-safe-area-context
react-native-screens

View File

@ -10,11 +10,13 @@
"app:android": "react-native run-android"
},
"dependencies": {
"@react-native-community/audio-toolkit": "git+https://github.com/tbenr/react-native-audio-toolkit.git#v2.0.3-status-v6",
"@react-native-community/cameraroll": "^1.6.1",
"@react-native-community/clipboard": "^1.2.2",
"@react-native-community/hooks": "^2.5.1",
"@react-native-community/masked-view": "^0.1.6",
"@react-native-community/netinfo": "^4.4.0",
"@react-native-community/slider": "^3.0.0",
"@react-navigation/bottom-tabs": "^5.7.0",
"@react-navigation/native": "^5.7.0",
"@react-navigation/stack": "^5.7.0",
@ -46,6 +48,7 @@
"react-native-languages": "^3.0.2",
"react-native-mail": "git+https://github.com/status-im/react-native-mail.git#v4.0.0-status",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-permissions": "^2.1.5",
"react-native-reanimated": "^1.7.0",
"react-native-redash": "^14.2.2",
"react-native-safe-area-context": "^2.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

View File

@ -0,0 +1,129 @@
(ns status-im.audio.core
(:require ["@react-native-community/audio-toolkit" :refer (Player Recorder MediaStates)]))
;; get mediastates from react module
(def PLAYING (.-PLAYING ^js MediaStates))
(def PAUSED (.-PAUSED ^js MediaStates))
(def RECORDING (.-RECORDING ^js MediaStates))
(def PREPARED (.-PREPARED ^js MediaStates))
(def IDLE (.-IDLE ^js MediaStates))
(def ERROR (.-ERROR ^js MediaStates))
(def DESTROYED (.-DESTROYED ^js MediaStates))
(def SEEKING (.-SEEKING ^js MediaStates))
(def default-recorder-options {:filename "recording.aac"
:bitrate 32000
:channels 1
:sampleRate 22050
:quality "medium" ; ios only
:meteringInterval 50})
(defn get-state [player-recorder]
(when player-recorder
(.-state ^js player-recorder)))
(defn new-recorder [options on-meter on-ended]
(let [recorder (new ^js Recorder
(:filename options)
(clj->js options))]
(when on-meter
(.on ^js recorder "meter" on-meter))
(when on-ended
(.on ^js recorder "ended" on-ended))))
(defn new-player [audio options on-ended]
(let [player (new ^js Player
audio
(clj->js options))]
(when on-ended
(.on ^js player "ended" on-ended))))
(defn prepare-player [player on-prepared on-error]
(when (and player (.-canPrepare ^js player))
(.prepare ^js player #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-prepared)))))
(defn prepare-recorder [recorder on-prepared on-error]
(when (and recorder (.-canPrepare ^js recorder))
(.prepare ^js recorder (fn [err _]
(if err
(on-error {:error (.-err err) :message (.-message err)})
(on-prepared))))))
(defn start-recording [recorder on-start on-error]
(when (and recorder
(or
(.-canRecord ^js recorder)
(.-canPrepare ^js recorder)))
(.record ^js recorder #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-start)))))
(defn stop-recording [recorder on-stop on-error]
(if (and recorder (#{RECORDING PAUSED} (get-state recorder)))
(.stop ^js recorder #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-stop)))
(on-stop)))
(defn pause-recording [recorder on-pause on-error]
(when (and recorder (.-isRecording ^js recorder))
(.pause ^js recorder #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-pause)))))
(defn start-playing [player on-start on-error]
(when (and player (.-canPlay ^js player))
(.play ^js player #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-start)))))
(defn stop-playing [player on-stop on-error]
(if (and player (.-isPlaying ^js player))
(.stop ^js player #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-stop)))
(on-stop)))
(defn get-recorder-file-path [recorder]
(when recorder
(.-fsPath ^js recorder)))
(defn get-player-duration [player]
(when (and player (.-canPlay ^js player))
(.-duration ^js player)))
(defn get-player-current-time [player]
(when (and player (.-canPlay ^js player))
(.-currentTime ^js player)))
(defn toggle-playpause-player [player on-play on-pause on-error]
(when (and player (.-canPlay ^js player))
(.playPause ^js player (fn [error pause?]
(if error
(on-error {:error (.-err error) :message (.-message error)})
(if pause?
(on-pause)
(on-play)))))))
(defn seek-player [player value on-seek on-error]
(when (and player (.-canPlay ^js player))
(.seek ^js player value #(if %
(on-error {:error (.-err %) :message (.-message %)})
(on-seek)))))
(defn can-play? [player]
(and player (.-canPlay ^js player)))
(defn destroy-recorder [recorder]
(stop-recording recorder
#(when (and recorder (not= (get-state recorder) DESTROYED))
(.destroy ^js recorder))
#()))
(defn destroy-player [player]
(stop-playing player
#(when (and player (not= (get-state player) IDLE))
(.destroy ^js player))
#()))

View File

@ -6,6 +6,7 @@
[status-im.chat.models.message :as chat.message]
[status-im.chat.models.message-content :as message-content]
[status-im.constants :as constants]
[status-im.i18n :as i18n]
[status-im.utils.datetime :as datetime]
[status-im.utils.fx :as fx]
["emojilib" :as emojis]))
@ -106,7 +107,16 @@
(chat.message/send-message {:chat-id current-chat-id
:content-type constants/content-type-image
:image-path (string/replace image-path #"file://" "")
:text "Update to latest version to see a nice image here!"})))))
:text (i18n/label :t/update-to-see-image)})))))
(fx/defn send-audio-message
[cofx audio-path duration current-chat-id]
(when-not (string/blank? audio-path)
(chat.message/send-message cofx {:chat-id current-chat-id
:content-type constants/content-type-audio
:audio-path audio-path
:audio-duration-ms duration
:text (i18n/label :t/update-to-listen-audio)})))
(fx/defn send-sticker-message
[cofx {:keys [hash pack]} current-chat-id]
@ -115,7 +125,7 @@
:content-type constants/content-type-sticker
:sticker {:hash hash
:pack pack}
:text "Update to latest version to see a nice sticker here!"})))
:text (i18n/label :t/update-to-see-sticker)})))
(fx/defn send-current-message
"Sends message from current chat input"

View File

@ -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)

View File

@ -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

View File

@ -560,6 +560,11 @@
{})
(chat.input/send-sticker-message sticker current-chat-id))))
(handlers/register-handler-fx
:chat/send-audio
(fn [{{:keys [current-chat-id]} :db :as cofx} [_ audio-path duration]]
(chat.input/send-audio-message cofx audio-path duration current-chat-id)))
(handlers/register-handler-fx
:chat/disable-cooldown
(fn [cofx _]
@ -1238,7 +1243,8 @@
(fx/defn on-going-in-background [{:keys [db now]}]
{:db (-> db
(dissoc :app-active-since)
(assoc :app-in-background-since now))})
(assoc :app-in-background-since now))
:dispatch-n [[:audio-recorder/on-background] [:audio-message/on-background]]})
(defn app-state-change [state {:keys [db] :as cofx}]
(let [app-coming-from-background? (= state "active")

View File

@ -364,3 +364,11 @@
[key-uid callback]
(log/debug "[native-module] delete-multiaccount")
(.deleteMultiaccount ^js (status) key-uid callback))
(defn activate-keep-awake []
(log/debug "[native-module] activateKeepAwake")
(.activateKeepAwake ^js (status)))
(defn deactivate-keep-awake []
(log/debug "[native-module] deactivateKeepAwake")
(.deactivateKeepAwake ^js (status)))

View File

@ -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

View File

@ -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

View File

@ -1,26 +1,33 @@
(ns status-im.ui.components.permissions
(:require [status-im.utils.platform :as platform]
["react-native" :refer (PermissionsAndroid)]))
["react-native-permissions" :refer (requestMultiple PERMISSIONS RESULTS)]))
(def permissions-map
{:read-external-storage "android.permission.READ_EXTERNAL_STORAGE"
:write-external-storage "android.permission.WRITE_EXTERNAL_STORAGE"
:camera "android.permission.CAMERA"})
{:read-external-storage (cond
platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
:write-external-storage (cond
platform/android? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
:camera (cond
platform/android? (.-CAMERA (.-ANDROID PERMISSIONS))
platform/ios? (.-CAMERA (.-IOS PERMISSIONS)))
:record-audio (cond
platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS))
platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))})
(defn all-granted? [permissions]
(let [permission-vals (distinct (vals permissions))]
(and (= (count permission-vals) 1)
(not (#{"denied" "never_ask_again"} (first permission-vals))))))
(not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals))))))
(defn request-permissions
[{:keys [permissions on-allowed on-denied]
:or {on-allowed #()
on-denied #()}}]
(if platform/android?
(let [permissions (mapv #(get permissions-map %) permissions)]
(-> (.requestMultiple PermissionsAndroid (clj->js permissions))
(let [permissions (remove nil? (mapv #(get permissions-map %) permissions))]
(if (empty? permissions)
(on-allowed)
(-> (requestMultiple (clj->js permissions))
(.then #(if (all-granted? (js->clj %))
(on-allowed)
(on-denied)))
(.catch on-denied)))
(on-allowed)))
(.catch on-denied)))))

View File

@ -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)))

View File

@ -0,0 +1,48 @@
(ns status-im.ui.screens.chat.audio-message.styles
(:require [status-im.ui.components.colors :as colors]))
(def container
{:flex 1
:flex-direction :column
:justify-content :space-around})
(def timer
{:font-size 28
:line-height 38
:align-self :center})
(def buttons-container
{:flex 1
:max-height 80
:flex-direction :row
:align-items :center
:justify-content :space-around
:align-self :stretch
:padding-horizontal 80})
(def rec-button-base-size 61)
(def rec-button-container
{:width rec-button-base-size
:height rec-button-base-size
:align-items "center"})
(defn rec-outer-circle [scale-anim]
{:position "absolute"
:width rec-button-base-size
:height rec-button-base-size
:top 0
:border-width 4
:transform [{:scale scale-anim}]
:border-color colors/red-audio-recorder
:border-radius rec-button-base-size})
(defn rec-inner-circle [scale-anim border-radius-anim]
{:position "absolute"
:top 6
:left 6
:bottom 6
:right 6
:transform [{:scale scale-anim}]
:border-radius border-radius-anim
:background-color colors/red-audio-recorder})

View File

@ -0,0 +1,292 @@
(ns status-im.ui.screens.chat.audio-message.views
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require
[goog.string :as gstring]
[reagent.core :as reagent]
[status-im.audio.core :as audio]
[status-im.ui.components.react :as react]
[re-frame.core :as re-frame]
[status-im.i18n :as i18n]
[quo.components.animated.pressable :as pressable]
[status-im.native-module.core :as status]
[status-im.ui.screens.chat.components.input :as input]
[status-im.ui.screens.chat.components.style :as input.style]
[status-im.ui.screens.chat.audio-message.styles :as styles]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.animation :as anim]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.utils.utils :as utils.utils]
[status-im.utils.fs :as fs]
[status-im.utils.fx :as fx]))
;; reference db levels
(def total-silence-db -160)
(def silence-db -35)
(def max-db 0)
;; update interval for the pulsing rec button
(def metering-interval 100)
;; rec pulse animation target
(defonce visual-target-value (anim/create-value total-silence-db))
;;ensure animation finishes before next meter update
(defonce metering-anim-duration (int (* metering-interval 0.9)))
(defn update-meter [meter-data]
(let [value (if meter-data
(.-value ^js meter-data)
total-silence-db)]
(anim/start (anim/timing visual-target-value {:toValue value
:duration metering-anim-duration
:useNativeDriver true}))))
(def base-filename "am.")
(def default-format "aac")
(def rec-options (merge
audio/default-recorder-options
{:filename (str base-filename default-format)
:meteringInterval metering-interval}))
;; maximum 2 minutes of recordings time
;; to keep data under 900k
(def max-recording-ms (* 2 60 1000))
;; audio objects
(defonce recorder-ref (atom nil))
(defonce player-ref (atom nil))
(defn destroy-recorder []
(audio/destroy-recorder @recorder-ref)
(reset! recorder-ref nil))
(defn destroy-player []
(audio/destroy-player @player-ref)
(reset! player-ref nil))
;; state update callback
(defonce state-cb (atom #()))
;; max recording ms reached callback
(defonce max-recording-reached-cb (atom #()))
;; to be called when app goes in background
(defonce on-background-cb (atom #()))
(fx/defn on-background
{:events [:audio-recorder/on-background]}
[_]
(when @on-background-cb
(@on-background-cb))
nil)
;; during recording
(defonce recording-timer (atom nil))
(defonce recording-start-ts (atom nil))
(defonce recording-backlog-ms (atom 0))
;; updates timer UI
(defn update-timer [timer]
(let [ms (if @recording-start-ts
(+
(- (js/Date.now) @recording-start-ts)
@recording-backlog-ms)
@recording-backlog-ms)
s (quot ms 1000)]
(if (> ms max-recording-ms)
(@max-recording-reached-cb)
(reset! timer (gstring/format "%d:%02d" (quot s 60) (mod s 60))))))
(defn reset-timer [timer]
(reset! timer "0:00")
(reset! recording-backlog-ms 0))
(defn animate-buttons [rec? show-ctrl? {:keys [rec-button-anim-value ctrl-buttons-anim-value]}]
(anim/start
(anim/parallel
[(anim/timing rec-button-anim-value {:toValue (if rec? 1 0)
:duration 100
:useNativeDriver true})
(anim/timing ctrl-buttons-anim-value {:toValue (if show-ctrl? 1 0)
:duration 100
:useNativeDriver true})])))
(defn start-recording [{:keys [timer] :as params}]
(if (> @recording-backlog-ms max-recording-ms)
(@max-recording-reached-cb)
(do
(animate-buttons true true params)
(reset! recording-start-ts (js/Date.now))
(reset! recording-timer (utils.utils/set-interval #(update-timer timer) 1000))
(audio/start-recording
@recorder-ref
@state-cb
#(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %))))))
(defn reload-recorder []
(when @recorder-ref
(destroy-recorder))
(reset! recorder-ref (audio/new-recorder rec-options #(update-meter %) @state-cb))
;; we skip preparation since if a recorder is prepared, player wont play
(@state-cb))
(defn reload-player
([] (reload-player nil))
([on-success]
(when @player-ref
(destroy-player))
(reset! player-ref (audio/new-player
(:filename rec-options)
{:autoDestroy false
:continuesToPlayInBackground false}
@state-cb))
(audio/prepare-player
@player-ref
#(do (@state-cb) (when on-success (on-success)))
#(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))))
(defn stop-recording [{:keys [on-success timer max-recording-reached?] :as params}]
(when @recording-timer
(utils.utils/clear-interval @recording-timer)
(reset! recording-timer nil))
(if max-recording-reached?
(reset! recording-backlog-ms (+ @recording-backlog-ms (- (js/Date.now) @recording-start-ts)))
(reset-timer timer))
(audio/stop-recording
@recorder-ref
#(do
(update-meter nil)
(reload-recorder)
(reload-player on-success))
#(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))
(animate-buttons false max-recording-reached? params))
(defn pause-recording [{:keys [timer] :as params}]
(when @recording-timer
(utils.utils/clear-interval @recording-timer)
(reset! recording-backlog-ms (+ @recording-backlog-ms (- (js/Date.now) @recording-start-ts)))
(reset! recording-start-ts nil)
(reset! recording-timer nil)
(update-timer timer))
(audio/pause-recording
@recorder-ref
#(do (update-meter nil)
(@state-cb))
#(utils.utils/show-popup (i18n/label :t/audio-recorder-error) (:message %)))
(animate-buttons false true params))
(defn update-state
"update main UI state.
general states are:
- :recording
- :playing
- :ready-to-send
- :recording-paused
- :ready-to-record"
[state-ref]
(let [player-state (audio/get-state @player-ref)
recorder-state (audio/get-state @recorder-ref)
output-file (or
(audio/get-recorder-file-path @recorder-ref)
(:output-file @state-ref))
general (cond
(= recorder-state audio/RECORDING) :recording
(= player-state audio/PLAYING) :playing
(= player-state audio/PREPARED) :ready-to-send
(= recorder-state audio/PAUSED) :recording-paused
:else :ready-to-record)
new-state {:general general
:cancel-disabled? (nil? (#{:recording :recording-paused :ready-to-send} general))
:output-file output-file
:duration (audio/get-player-duration @player-ref)}]
(if (#{:recording :recording-paused} general)
(status/activate-keep-awake)
(status/deactivate-keep-awake))
(when (not= @state-ref new-state)
(reset! state-ref new-state))))
(defn send-audio-msessage [state-ref]
(re-frame/dispatch [:chat/send-audio
(:output-file @state-ref)
(int (:duration @state-ref))])
(destroy-player)
(@state-cb))
;; rec-button-anim-value 0 => stopped, 1 => recording
(defview rec-button-view [{:keys [rec-button-anim-value state] :as params}]
(letsubs [outer-scale (anim/interpolate visual-target-value {:inputRange [total-silence-db silence-db 0]
:outputRange [1 0.8 1.2]})
inner-scale (anim/interpolate rec-button-anim-value {:inputRange [0 1]
:outputRange [1 0.5]})
inner-border-radius (anim/interpolate rec-button-anim-value {:inputRange [0 1]
:outputRange [styles/rec-button-base-size 16]})]
[react/touchable-highlight {:on-press #(if (= (:general @state) :recording)
(pause-recording params)
(start-recording params))}
[react/view {:style styles/rec-button-container}
[react/animated-view {:style (styles/rec-outer-circle outer-scale)}]
[react/animated-view {:style (styles/rec-inner-circle inner-scale inner-border-radius)}]]]))
(defn- cancel-button [disabled? on-press]
[pressable/pressable {:type :scale
:disabled disabled?
:on-press on-press}
[react/view {:style (input.style/send-message-button)}
[icons/icon :main-icons/close
{:container-style (merge (input.style/send-message-container) {:background-color colors/gray})
:accessibility-label :cancel-message-button
:color colors/white-persist}]]])
(defview audio-message-view []
(letsubs [rec-button-anim-value (anim/create-value 0)
ctrl-buttons-anim-value (anim/create-value 0)
timer (reagent/atom "")
state (reagent/atom nil)]
{:component-did-mount (fn []
(reset-timer timer)
(reset! state-cb #(update-state state))
(reset! max-recording-reached-cb #(do
(when (= (:general @state) :recording)
(stop-recording {:rec-button-anim-value rec-button-anim-value
:ctrl-buttons-anim-value ctrl-buttons-anim-value
:timer timer
:max-recording-reached? true}))
(utils.utils/show-popup (i18n/label :t/audio-recorder)
(i18n/label :t/audio-recorder-max-ms-reached))))
(reset! on-background-cb #(when (= (:general @state) :recording)
(pause-recording {:rec-button-anim-value rec-button-anim-value
:ctrl-buttons-anim-value ctrl-buttons-anim-value
:timer timer})))
(reload-recorder))
:component-will-unmount (fn []
(when @recording-timer
(utils.utils/clear-interval @recording-timer)
(reset! recording-timer nil))
(destroy-recorder)
(destroy-player)
(when (:output-file @state)
; possible issue if message is not yet sent?
(fs/unlink (:output-file @state)))
(reset! state-cb nil)
(reset! max-recording-reached-cb nil)
(reset! on-background-cb nil))}
(let [base-params {:rec-button-anim-value rec-button-anim-value
:ctrl-buttons-anim-value ctrl-buttons-anim-value
:timer timer}]
[react/view {:style styles/container}
[react/text {:style styles/timer} @timer]
[react/view {:style styles/buttons-container}
[react/animated-view {:style {:opacity ctrl-buttons-anim-value}}
[cancel-button (:cancel-disabled? @state) #(stop-recording base-params)]]
[rec-button-view (merge base-params {:state state})]
[react/animated-view {:style {:opacity ctrl-buttons-anim-value}}
[input/send-button {:on-send-press (fn [] (cond
(= :ready-to-send (:general @state))
(do
(reset-timer timer)
(animate-buttons false false base-params)
(send-audio-msessage state))
(#{:recording :recording-paused} (:general @state))
(stop-recording (merge base-params
{:on-success
#(send-audio-msessage state)}))))}]]]])))

View File

@ -6,6 +6,7 @@
[quo.design-system.colors :as colors]
[status-im.ui.screens.chat.components.style :as styles]
[status-im.ui.screens.chat.components.reply :as reply]
[status-im.utils.utils :as utils.utils]
[quo.components.animated.pressable :as pressable]
[quo.animated :as animated]
[status-im.utils.config :as config]
@ -37,12 +38,25 @@
[icons/icon :main-icons/keyboard (styles/icon false)]
[icons/icon :main-icons/stickers (styles/icon false)])]])
(defn- request-record-audio-permission [set-active panel]
(re-frame/dispatch
[:request-permissions
{:permissions [:record-audio]
:on-allowed
#(set-active panel)
:on-denied
#(utils.utils/set-timeout
(fn []
(utils.utils/show-popup (i18n/label :t/audio-recorder-error)
(i18n/label :t/audio-recorder-permissions-error)))
50)}]))
(defn touchable-audio-icon [{:keys [panel active set-active accessibility-label input-focus]}]
[pressable/pressable {:type :scale
:accessibility-label accessibility-label
:on-press #(if (= active panel)
(input-focus)
(set-active panel))}
(request-record-audio-permission set-active panel))}
[rn/view {:style (styles/in-input-touchable-icon)}
[icons/icon
(panel->icons panel)
@ -116,6 +130,7 @@
[touchable-audio-icon {:panel :audio
:accessibility-label :show-audio-message-icon
:active active-panel
:input-focus input-focus
:set-active set-active-panel}])]]]])
(defn chat-toolbar []
@ -151,8 +166,8 @@
one-to-one-chat?
(or config/commands-enabled? mainnet?)
(not reply))
show-audio (and false ;TODO: remove to enable audio icon
empty-text
show-audio (and empty-text
(not sending-image)
(not reply)
(not public?))]
(when-not (= reply @had-reply)

View File

@ -103,7 +103,8 @@
:align-items :center})
(defn in-input-buttons []
{:height 34})
{:flex-direction :row
:height 34})
(defn send-icon-color []
colors/white)

View File

@ -0,0 +1,231 @@
(ns status-im.ui.screens.chat.message.audio
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [status-im.utils.utils :as utils]
[reagent.core :as reagent]
[goog.string :as gstring]
[status-im.audio.core :as audio]
[status-im.utils.fx :as fx]
[status-im.ui.screens.chat.styles.message.audio :as style]
[status-im.ui.components.animation :as anim]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.ui.components.react :as react]
[status-im.ui.components.slider :as slider]))
(defn message-press-handlers [_]
;;TBI save audio file?
)
(defonce player-ref (atom nil))
(defonce current-player-message-id (atom nil))
(defonce current-active-state-ref-ref (atom nil))
(defonce progress-timer (atom nil))
(defn start-stop-progress-timer [{:keys [state-ref progress-ref progress-anim]} start?]
(when @progress-timer
(utils/clear-interval @progress-timer)
(when-not start?
(reset! progress-timer nil)))
(when start?
(when @progress-timer
(utils/clear-interval @progress-timer))
(reset! progress-timer (utils/set-interval
#(when (and @state-ref (not (:slider-seeking @state-ref)))
(let [ct (audio/get-player-current-time @player-ref)]
(reset! progress-ref ct)
(when ct
(anim/start (anim/timing progress-anim {:toValue @progress-ref
:duration 100
:easing (.-linear ^js anim/easing)
:useNativeDriver true})))))
100))))
(defn update-state [{:keys [state-ref progress-ref progress-anim message-id seek-to-ms audio-duration-ms slider-new-state-seeking? unloaded? error]}]
(let [player-state (audio/get-state @player-ref)
slider-seeking (if (some? slider-new-state-seeking?)
slider-new-state-seeking?
(:slider-seeking @state-ref))
general (cond
(some? error) :error
(or unloaded? (not= message-id @current-player-message-id)) :not-loaded
slider-seeking (:general @state-ref) ; persist player state at the time user started sliding
(= player-state audio/PLAYING) :playing
(= player-state audio/PAUSED) :paused
(= player-state audio/SEEKING) :seeking
(= player-state audio/PREPARED) :ready-to-play
:else :preparing)
new-state {:general general
:error-msg error
:duration (cond (not (#{:preparing :not-loaded :error} general))
(audio/get-player-duration @player-ref)
audio-duration-ms audio-duration-ms
:else (:duration @state-ref))
:progress-ref (or progress-ref (:progress-ref @state-ref))
:progress-anim (or progress-anim (:progress-anim @state-ref))
:slider-seeking slider-seeking
; persist seek-to-ms while seeking or audio is not loaded
:seek-to-ms (when (or
slider-seeking
(#{:preparing :not-loaded :error} general))
(or seek-to-ms (:seek-to-ms @state-ref)))}]
; update state if needed
(when (not= @state-ref new-state)
(reset! state-ref new-state))
; update progress UI on slider release
(when (and (some? slider-new-state-seeking?) (not slider-new-state-seeking?) (some? seek-to-ms))
(reset! (:progress-ref new-state) seek-to-ms))
; update progres anim value to follow the slider
(when (and slider-seeking (some? seek-to-ms))
(anim/set-value (:progress-anim new-state) seek-to-ms))
; on unload, reset values
(when unloaded?
(reset! (:progress-ref new-state) 0)
(anim/set-value (:progress-anim new-state) 0))))
(defn destroy-player [{:keys [message-id reloading?]}]
(when (and @player-ref (or reloading?
(= message-id @current-player-message-id)))
(audio/destroy-player @player-ref)
(reset! player-ref nil)
(when @current-active-state-ref-ref
(update-state {:state-ref @current-active-state-ref-ref :unloaded? true}))
(reset! current-player-message-id nil)
(reset! current-active-state-ref-ref nil)))
(defonce last-seek (atom (js/Date.now)))
(defn seek [{:keys [message-id] :as params} value immediate? on-success]
(when (and @player-ref (= message-id @current-player-message-id))
(let [now (js/Date.now)]
(when (or immediate? (> (- now @last-seek) 200))
(reset! last-seek (js/Date.now))
(audio/seek-player
@player-ref
value
#(do
(update-state params)
(when on-success (on-success)))
#(update-state (merge params {:error (:message %)}))))))
(update-state (merge params {:seek-to-ms value})))
(defn reload-player [{:keys [message-id state-ref] :as params} base64-data on-success]
;; to avoid reloading player while is initializing,
;; we go ahead only if there is no player or
;; if it is already prepared
(when (or (nil? @player-ref) (audio/can-play? @player-ref))
(when @player-ref
(destroy-player (merge params {:reloading? true})))
(reset! player-ref (audio/new-player
base64-data
{:autoDestroy false
:continuesToPlayInBackground false}
#(seek params 0 true nil)))
(audio/prepare-player
@player-ref
#(when on-success (on-success))
#(update-state (merge params {:error (:message %)})))
(reset! current-player-message-id message-id)
(reset! current-active-state-ref-ref state-ref)
(update-state params)))
(defn play-pause [{:keys [message-id state-ref] :as params} audio]
(if (not= message-id @current-player-message-id)
;; player has audio from another message, we need to reload
(reload-player params
audio
;; on-success: audio is loaded, do we have an existing value to seek to?
#(if-some [seek-time (:seek-to-ms @state-ref)]
;; check seek time against real audio duration and play
(let [checked-seek-time (min (audio/get-player-duration @player-ref) seek-time)]
(seek params
checked-seek-time
true
(fn [] (play-pause params audio))))
;; nothing to seek to, play
(play-pause params audio)))
;; loaded audio corresponds to current message we can play
(when @player-ref
(audio/toggle-playpause-player
@player-ref
#(do
(start-stop-progress-timer params true)
(update-state params))
#(do
(start-stop-progress-timer params false)
(update-state params))
#(update-state (merge params {:error (:message %)}))))))
(defn- play-pause-button [state-ref outgoing on-press]
(let [color (if outgoing colors/blue colors/white-persist)]
(if (= (:general @state-ref) :preparing)
[react/view {:style (style/play-pause-container outgoing true)}
[react/small-loading-indicator color]]
[react/touchable-highlight {:on-press on-press}
[icons/icon (case (:general @state-ref)
:playing :main-icons/pause
:main-icons/play)
{:container-style (style/play-pause-container outgoing false)
:accessibility-label :play-pause-audio-message-button
:color color}]])))
(fx/defn on-background
{:events [:audio-message/on-background]}
[_]
(when (and @current-active-state-ref-ref
@@current-active-state-ref-ref)
(update-state {:state-ref @current-active-state-ref-ref
:message-id @current-player-message-id}))
nil)
(defview message-content [{:keys [audio audio-duration-ms message-id outgoing]} timestamp-view]
(letsubs [state (reagent/atom nil)
progress (reagent/atom 0)
progress-anim (anim/create-value 0)
width [:dimensions/window-width]]
{:component-did-mount (fn []
(update-state {:state-ref state
:audio-duration-ms audio-duration-ms
:message-id message-id
:unloaded? true
:progress-ref progress
:progress-anim progress-anim}))
:component-will-unmount (fn []
(destroy-player {:state-ref state :message-id message-id})
(when (= @current-player-message-id message-id)
(reset! current-active-state-ref-ref nil)
(reset! current-player-message-id nil))
(reset! state nil))}
(let [base-params {:state-ref state :message-id message-id :progress-ref progress :progress-anim progress-anim}]
(if (= (:general @state) :error)
[react/text {:style {:typography :main-medium
:margin-bottom 16}} (:error-msg @state)]
[react/view (style/container width)
[react/view style/play-pause-slider-container
[play-pause-button state outgoing #(play-pause base-params audio)]
[react/view style/slider-container
[slider/animated-slider (merge (style/slider outgoing)
{:minimum-value 0
:maximum-value (:duration @state)
:value progress-anim
:on-value-change #(seek base-params % false nil)
:on-sliding-start #(seek (merge base-params {:slider-new-state-seeking? true}) % true nil)
:on-sliding-complete #(seek (merge base-params {:slider-new-state-seeking? false}) % true nil)})]]]
[react/view style/times-container
[react/text {:style (style/timestamp outgoing)}
(let [time (cond
(or (:slider-seeking @state) (> (:seek-to-ms @state) 0)) (:seek-to-ms @state)
(#{:playing :paused :seeking} (:general @state)) @progress
:else (:duration @state))
s (quot time 1000)]
(gstring/format "%02d:%02d" (quot s 60) (mod s 60)))]
timestamp-view]]))))

View File

@ -5,6 +5,7 @@
[status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.react :as react]
[status-im.ui.screens.chat.message.audio :as message.audio]
[status-im.ui.screens.chat.message.command :as message.command]
[status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.chat.sheets :as sheets]
@ -289,6 +290,12 @@
{:content (sheets/sticker-long-press message)
:height 64}])}))
(defn message-content-audio
[message]
[react/touchable-highlight (message.audio/message-press-handlers message)
[message-bubble-wrapper message
[message.audio/message-content message [message-timestamp message false]]]])
(defn chat-message [{:keys [public? content content-type] :as message}]
(if (= content-type constants/content-type-command)
[message.command/command-content message-content-wrapper message]
@ -313,4 +320,8 @@
(not public?))
[react/touchable-highlight (image-message-press-handlers message)
[message-content-image message]]
[unknown-content-type message])))))])))
(if (and (= content-type constants/content-type-audio)
;; Disabling audio for public-chats
(not public?))
[message-content-audio message]
[unknown-content-type message]))))))])))

View File

@ -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}))

View File

@ -11,6 +11,7 @@
[status-im.ui.screens.chat.sheets :as sheets]
[quo.animated :as animated]
[quo.react-native :as rn]
[status-im.ui.screens.chat.audio-message.views :as audio-message]
[status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.stickers.views :as stickers]
[status-im.ui.screens.chat.styles.main :as style]
@ -177,6 +178,8 @@
[extensions/extensions-view]
:images
[image/image-view]
:audio
[audio-message/audio-message-view]
nil))
(defn chat []

View File

@ -87,6 +87,11 @@
:accessibility-label :no-messages-text}
(i18n/label :t/image)]
(= constants/content-type-audio content-type)
[react/text {:style styles/last-message-text
:accessibility-label :no-messages-text}
(i18n/label :t/audio)]
(string/blank? (:text content))
[react/text {:style styles/last-message-text}
""]

View File

@ -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)

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
"owner": "status-im",
"repo": "status-go",
"version": "v0.55.2",
"commit-sha1": "0b3cdf7362bbdf9ba7fc11da803105f9417dfbac",
"src-sha256": "1vq3z150p0fbwjc1mqmi8lz4vg28dzqhlpsn7kar8j5z4rx5z5hn"
"version": "v0.56.1",
"commit-sha1": "4574ab4c22ee6b662a7f87c39d0f714998c567dc",
"src-sha256": "0jd684lv7x93gxgvvhsv4ihxr5sw2rck2divsxfiql5g2c1v4alg"
}

View File

@ -1169,5 +1169,13 @@
"private-notifications": "Private notifications",
"private-notifications-descr": "Status will notify you about new messages. You can edit your notification preferences later in settings.",
"maybe-later": "Maybe later",
"join": "Join"
"join": "Join",
"audio-recorder-error": "Recorder error",
"audio-recorder": "Recorder",
"audio-recorder-max-ms-reached": "Maximum recording time reached",
"audio-recorder-permissions-error": "You have to give permission to send audio messages",
"audio": "Audio",
"update-to-see-image": "Update to latest version to see a nice image here!",
"update-to-listen-audio": "Update to latest version to listen to an audio message here!",
"update-to-see-sticker": "Update to latest version to see a nice sticker here!"
}

View File

@ -1207,6 +1207,14 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@react-native-community/audio-toolkit@git+https://github.com/tbenr/react-native-audio-toolkit.git#v2.0.3-status-v6":
version "2.0.3"
resolved "git+https://github.com/tbenr/react-native-audio-toolkit.git#7ae9055cf6169b30f5089bda7bfcfc1c40a715e5"
dependencies:
async "^2.6.3"
eventemitter3 "^1.2.0"
lodash "^4.17.15"
"@react-native-community/cameraroll@^1.6.1":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cameraroll/-/cameraroll-1.6.2.tgz#a4dedcf8ba7bc938f805dd07dd43a275edb1f411"
@ -1329,6 +1337,11 @@
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-4.7.0.tgz#7482d36836cac69d0a0ae25581f65bc472639930"
integrity sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==
"@react-native-community/slider@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.0.tgz#ffbf78689fc0572fb5c1e2ccb61b2ef074d3dcd2"
integrity sha512-deNc3JHBHz24YN+0DTAocXfrYFIFc1DvsIriMJSsJlR/MvsLzoq2+qwaEN+0/LJ37pstv85wZWY0pNugk4e41g==
"@react-navigation/bottom-tabs@^5.7.0":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.7.1.tgz#d6b4e61676f8b4ab11864e792c819c56fde32a44"
@ -1822,7 +1835,7 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@^2.4.0:
async@^2.4.0, async@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@ -3251,6 +3264,11 @@ event-target-shim@^5.0.0, event-target-shim@^5.0.1:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=
eventemitter3@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
@ -6554,6 +6572,11 @@ react-native-navigation-bar-color@^2.0.1:
resolved "https://registry.yarnpkg.com/react-native-navigation-bar-color/-/react-native-navigation-bar-color-2.0.1.tgz#ee2be25cc37105f7da355717b0a9a32c9c059ae6"
integrity sha512-1kE/oxWt+HYjRxdZdvke9tJ365xaee5n3+euOQA1En8zQuSbOxiE4SYEGM7TeaWnmLJ0l37mRnPHaB2H4mGh0A==
react-native-permissions@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-2.1.5.tgz#6cfc4f1ab1590f4952299b7cdc9698525ad540e0"
integrity sha512-b9KO/4UEV9qddl+kcSybmdk8nlAifclSDBR2rSvc5KZM06vIaJWJNIzK2ZwPXqDQ5yD3CJLuKTRj7Fz+jM9qyQ==
react-native-reanimated@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-1.8.0.tgz#0b5719b20c1fed9aaf8afd9a12e21c9bd46ee428"