migrate reagent record audio component part5 (#19212)

This commit is contained in:
flexsurfer 2024-03-13 14:27:22 +01:00 committed by GitHub
parent 296d868797
commit 1a8bca56e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1064 additions and 927 deletions

View File

@ -4,10 +4,10 @@
[quo.components.record-audio.record-audio.helpers :as helpers] [quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[react-native.core :refer [use-effect]] [react-native.core :as rn]
[react-native.reanimated :as reanimated])) [react-native.reanimated :as reanimated]))
(defn f-delete-button (defn delete-button
[recording? ready-to-delete? reviewing-audio? force-show-controls?] [recording? ready-to-delete? reviewing-audio? force-show-controls?]
(let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0)) (let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0))
translate-x (reanimated/use-shared-value (if force-show-controls? 35 20)) translate-x (reanimated/use-shared-value (if force-show-controls? 35 20))
@ -17,62 +17,68 @@
connector-height (reanimated/use-shared-value 12) connector-height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8) border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8) border-radius-second-half (reanimated/use-shared-value 8)
start-x-animation (fn [] start-x-animation (rn/use-callback
(helpers/animate-linear-with-delay translate-x 12 50 133.33) (fn []
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33) (helpers/animate-linear-with-delay translate-x 12 50 133.33)
(helpers/animate-easing-with-delay connector-width 56 83.33 80) (helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay connector-height 56 83.33 80) (helpers/animate-easing-with-delay connector-width 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half (helpers/animate-easing-with-delay connector-height 56 83.33 80)
28 (helpers/animate-easing-with-delay border-radius-first-half
83.33 28
80) 83.33
(helpers/animate-easing-with-delay border-radius-second-half 80)
28 (helpers/animate-easing-with-delay border-radius-second-half
83.33 28
80)) 83.33
reset-x-animation (fn [] 80)))
(helpers/animate-linear translate-x 0 100) reset-x-animation (rn/use-callback
(helpers/set-value connector-opacity 0) (fn []
(helpers/set-value connector-width 24) (helpers/animate-linear translate-x 0 100)
(helpers/set-value connector-height 12) (helpers/set-value connector-opacity 0)
(helpers/set-value border-radius-first-half 8) (helpers/set-value connector-width 24)
(helpers/set-value border-radius-second-half 16)) (helpers/set-value connector-height 12)
fade-in-animation (fn [] (helpers/set-value border-radius-first-half 8)
(helpers/animate-linear translate-x 0 200) (helpers/set-value border-radius-second-half 16)))
(helpers/animate-linear opacity 1 200)) fade-in-animation (rn/use-callback
fade-out-animation (fn [] (fn []
(helpers/animate-linear (helpers/animate-linear translate-x 0 200)
translate-x (helpers/animate-linear opacity 1 200)))
(if @reviewing-audio? 35 20) fade-out-animation (rn/use-callback
200) (fn []
(if @reviewing-audio? (helpers/animate-linear
(helpers/animate-linear scale 0.75 200) translate-x
(helpers/animate-linear opacity 0 200)) (if reviewing-audio? 35 20)
(helpers/set-value connector-opacity 0) 200)
(helpers/set-value connector-width 24) (if reviewing-audio?
(helpers/set-value connector-height 12) (helpers/animate-linear scale 0.75 200)
(helpers/set-value border-radius-first-half 8) (helpers/animate-linear opacity 0 200))
(helpers/set-value border-radius-second-half 16)) (helpers/set-value connector-opacity 0)
fade-out-reset-animation (fn [] (helpers/set-value connector-width 24)
(helpers/animate-linear opacity 0 200) (helpers/set-value connector-height 12)
(helpers/animate-linear-with-delay translate-x 20 0 200) (helpers/set-value border-radius-first-half 8)
(helpers/animate-linear-with-delay scale 1 0 200))] (helpers/set-value border-radius-second-half 16))
(use-effect (fn [] [reviewing-audio?])
(if @recording? fade-out-reset-animation (rn/use-callback
(fade-in-animation) (fn []
(fade-out-animation))) (helpers/animate-linear opacity 0 200)
[@recording?]) (helpers/animate-linear-with-delay translate-x 20 0 200)
(use-effect (fn [] (helpers/animate-linear-with-delay scale 1 0 200)))]
(when-not @reviewing-audio? (rn/use-effect (fn []
(fade-out-reset-animation))) (if recording?
[@reviewing-audio?]) (fade-in-animation)
(use-effect (fn [] (fade-out-animation)))
(cond [recording?])
@ready-to-delete? (rn/use-effect (fn []
(start-x-animation) (when-not reviewing-audio?
@recording? (fade-out-reset-animation)))
(reset-x-animation))) [reviewing-audio?])
[@ready-to-delete?]) (rn/use-effect (fn []
(cond
ready-to-delete?
(start-x-animation)
recording?
(reset-x-animation)))
[ready-to-delete?])
[:<> [:<>
[reanimated/view {:style (style/delete-button-container opacity)} [reanimated/view {:style (style/delete-button-container opacity)}
[reanimated/view [reanimated/view

View File

@ -5,10 +5,10 @@
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme :as quo.theme] [quo.theme :as quo.theme]
[react-native.core :refer [use-effect]] [react-native.core :as rn]
[react-native.reanimated :as reanimated])) [react-native.reanimated :as reanimated]))
(defn f-lock-button (defn lock-button
[recording? ready-to-lock? locked?] [recording? ready-to-lock? locked?]
(let [theme (quo.theme/use-theme-value) (let [theme (quo.theme/use-theme-value)
translate-x-y (reanimated/use-shared-value 20) translate-x-y (reanimated/use-shared-value 20)
@ -18,54 +18,58 @@
height (reanimated/use-shared-value 12) height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8) border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8) border-radius-second-half (reanimated/use-shared-value 8)
start-x-y-animation (fn [] start-x-y-animation (rn/use-callback
(helpers/animate-linear-with-delay translate-x-y 8 50 116.66) (fn []
(helpers/animate-easing-with-delay connector-opacity 1 0 80) (helpers/animate-linear-with-delay translate-x-y 8 50 116.66)
(helpers/animate-easing-with-delay width 56 83.33 63.33) (helpers/animate-easing-with-delay connector-opacity 1 0 80)
(helpers/animate-easing-with-delay height 56 83.33 63.33) (helpers/animate-easing-with-delay width 56 83.33 63.33)
(helpers/animate-easing-with-delay border-radius-first-half (helpers/animate-easing-with-delay height 56 83.33 63.33)
28 (helpers/animate-easing-with-delay border-radius-first-half
83.33 28
63.33) 83.33
(helpers/animate-easing-with-delay border-radius-second-half 63.33)
28 (helpers/animate-easing-with-delay border-radius-second-half
83.33 28
63.33)) 83.33
reset-x-y-animation (fn [] 63.33)))
(helpers/animate-linear translate-x-y 0 100) reset-x-y-animation (rn/use-callback
(helpers/set-value connector-opacity 0) (fn []
(helpers/set-value width 24) (helpers/animate-linear translate-x-y 0 100)
(helpers/set-value height 12) (helpers/set-value connector-opacity 0)
(helpers/set-value border-radius-first-half 8) (helpers/set-value width 24)
(helpers/set-value border-radius-second-half 16)) (helpers/set-value height 12)
fade-in-animation (fn [] (helpers/set-value border-radius-first-half 8)
(helpers/animate-linear translate-x-y 0 220) (helpers/set-value border-radius-second-half 16)))
(helpers/animate-linear opacity 1 220)) fade-in-animation (rn/use-callback
fade-out-animation (fn [] (fn []
(helpers/animate-linear translate-x-y 20 200) (helpers/animate-linear translate-x-y 0 220)
(helpers/animate-linear opacity 0 200) (helpers/animate-linear opacity 1 220)))
(helpers/set-value connector-opacity 0) fade-out-animation (rn/use-callback
(helpers/set-value width 24) (fn []
(helpers/set-value height 12) (helpers/animate-linear translate-x-y 20 200)
(helpers/set-value border-radius-first-half 8) (helpers/animate-linear opacity 0 200)
(helpers/set-value border-radius-second-half 16))] (helpers/set-value connector-opacity 0)
(use-effect (fn [] (helpers/set-value width 24)
(if @recording? (helpers/set-value height 12)
(fade-in-animation) (helpers/set-value border-radius-first-half 8)
(fade-out-animation))) (helpers/set-value border-radius-second-half 16)))]
[@recording?]) (rn/use-effect (fn []
(use-effect (fn [] (if recording?
(cond (fade-in-animation)
@ready-to-lock? (fade-out-animation)))
(start-x-y-animation) [recording?])
(and @recording? (not @locked?)) (rn/use-effect (fn []
(reset-x-y-animation))) (cond
[@ready-to-lock?]) ready-to-lock?
(use-effect (fn [] (start-x-y-animation)
(if @locked? (and recording? (not locked?))
(fade-out-animation) (reset-x-y-animation)))
(reset-x-y-animation))) [ready-to-lock?])
[@locked?]) (rn/use-effect (fn []
(if locked?
(fade-out-animation)
(reset-x-y-animation)))
[locked?])
[:<> [:<>
[reanimated/view {:style (style/lock-button-container opacity)} [reanimated/view {:style (style/lock-button-container opacity)}
[reanimated/view [reanimated/view
@ -77,6 +81,6 @@
[reanimated/view [reanimated/view
{:style (style/lock-button translate-x-y opacity) {:style (style/lock-button translate-x-y opacity)
:pointer-events :none} :pointer-events :none}
[icons/icon (if @ready-to-lock? :i/locked :i/unlocked) [icons/icon (if ready-to-lock? :i/locked :i/unlocked)
{:color (colors/theme-colors colors/black colors/white theme) {:color (colors/theme-colors colors/black colors/white theme)
:size 20}]]])) :size 20}]]]))

View File

@ -3,19 +3,17 @@
[quo.components.buttons.button.view :as button] [quo.components.buttons.button.view :as button]
[quo.components.record-audio.record-audio.helpers :as helpers] [quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[react-native.core :as rn :refer [use-effect]] [react-native.core :as rn]
[react-native.reanimated :as reanimated])) [react-native.reanimated :as reanimated]))
(defn f-record-button (defn record-button
[recording? reviewing-audio?] [recording? reviewing-audio?]
(let [opacity (reanimated/use-shared-value 1) (let [opacity (reanimated/use-shared-value 1)]
show-animation #(helpers/set-value opacity 1) (rn/use-effect (fn []
hide-animation #(helpers/set-value opacity 0)] (if (or recording? reviewing-audio?)
(use-effect (fn [] (helpers/set-value opacity 0)
(if (or @recording? @reviewing-audio?) (helpers/set-value opacity 1)))
(hide-animation) [recording? reviewing-audio?])
(show-animation)))
[@recording? @reviewing-audio?])
[reanimated/view {:style (style/record-button-container opacity)} [reanimated/view {:style (style/record-button-container opacity)}
[button/button [button/button
{:type :outline {:type :outline

View File

@ -5,8 +5,9 @@
[quo.components.record-audio.record-audio.helpers :as helpers] [quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme]
[react-native.audio-toolkit :as audio] [react-native.audio-toolkit :as audio]
[react-native.core :as rn :refer [use-effect]] [react-native.core :as rn]
[react-native.reanimated :as reanimated] [react-native.reanimated :as reanimated]
[reagent.core :as reagent] [reagent.core :as reagent]
[taoensso.timbre :as log] [taoensso.timbre :as log]
@ -28,13 +29,16 @@
(reagent/as-element (reagent/as-element
[reanimated/view {:style (style/animated-circle scale opacity color)}])))))) [reanimated/view {:style (style/animated-circle scale opacity color)}]))))))
(defn f-record-button-big (defn record-button-big
[{:keys [recording? ready-to-send? ready-to-lock? ready-to-delete? record-button-is-animating? [{:keys [recording? set-recording ready-to-send? set-ready-to-send ready-to-lock? set-ready-to-lock
record-button-at-initial-position? locked? reviewing-audio? recording-length-ms ready-to-delete? set-ready-to-delete record-button-is-animating?
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel theme]}] record-button-at-initial-position? locked? set-locked reviewing-audio? recording-length-ms
(let [scale (reanimated/use-shared-value 1) set-recording-length-ms
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel]}]
(let [theme (quo.theme/use-theme-value)
scale (reanimated/use-shared-value 1)
opacity (reanimated/use-shared-value 0) opacity (reanimated/use-shared-value 0)
opacity-from (if @ready-to-lock? opacity-from-lock opacity-from-default) opacity-from (if ready-to-lock? opacity-from-lock opacity-from-default)
animations (map animations (map
(fn [index] (fn [index]
(let [ring-scale (worklets.record-audio/ring-scale scale (let [ring-scale (worklets.record-audio/ring-scale scale
@ -46,141 +50,155 @@
[opacity-from 0])})) [opacity-from 0])}))
(range 0 5)) (range 0 5))
rings-color (cond rings-color (cond
@ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque
colors/neutral-80 colors/neutral-80
theme) theme)
@ready-to-delete? colors/danger-50 ready-to-delete? colors/danger-50
:else colors/primary-50) :else colors/primary-50)
translate-y (reanimated/use-shared-value 0) translate-y (reanimated/use-shared-value 0)
translate-x (reanimated/use-shared-value 0) translate-x (reanimated/use-shared-value 0)
button-color colors/primary-50 button-color colors/primary-50
icon-color (if (and (not (= :dark theme)) @ready-to-lock?) colors/black colors/white) icon-color (if (and (not (= :dark theme)) ready-to-lock?) colors/black colors/white)
icon-opacity (reanimated/use-shared-value 1) icon-opacity (reanimated/use-shared-value 1)
red-overlay-opacity (reanimated/use-shared-value 0) red-overlay-opacity (reanimated/use-shared-value 0)
gray-overlay-opacity (reanimated/use-shared-value 0) gray-overlay-opacity (reanimated/use-shared-value 0)
complete-animation complete-animation
(fn [] (rn/use-callback
(cond (fn []
(and @ready-to-lock? (not @record-button-is-animating?)) (cond
(do (and ready-to-lock? (not @record-button-is-animating?))
(reset! locked? true) (do
(reset! ready-to-lock? false)) (set-locked true)
(and (not @locked?) (not @reviewing-audio?)) (set-ready-to-lock false))
(audio/stop-recording (and (not locked?) (not reviewing-audio?))
@recorder-ref (audio/stop-recording
(fn [] @recorder-ref
(cond (fn []
@ready-to-send? (cond
(when on-send ready-to-send?
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref) (when on-send
:duration @recording-length-ms})) (on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
@ready-to-delete? :duration recording-length-ms}))
(when on-cancel ready-to-delete?
(on-cancel))) (when on-cancel
(reload-recorder-fn) (on-cancel)))
(reset! recording? false) (reload-recorder-fn)
(reset! ready-to-send? false) (set-recording false)
(reset! ready-to-delete? false) (set-ready-to-send false)
(reset! ready-to-lock? false) (set-ready-to-delete false)
(reset! idle? true) (set-ready-to-lock false)
(js/setTimeout #(reset! idle? false) 1000) (reset! idle? true)
(reset! recording-length-ms 0) (js/setTimeout #(reset! idle? false) 1000)
(log/debug "[record-audio] stop recording - success")) (set-recording-length-ms 0)
#(log/error "[record-audio] stop recording - error: " %)))) (log/debug "[record-audio] stop recording - success"))
start-animation (fn [] #(log/error "[record-audio] stop recording - error: " %))))
(helpers/set-value opacity 1) [ready-to-lock? locked? reviewing-audio? ready-to-send? recording-length-ms ready-to-delete?
(helpers/animate-linear scale 2.6 signal-anim-duration) idle?])
;; TODO: Research if we can implement this with withSequence method start-animation (rn/use-callback
;; from Reanimated 2 (fn []
;; GitHub issue [#14561]: (helpers/set-value opacity 1)
;; https://github.com/status-im/status-mobile/issues/14561 (helpers/animate-linear scale 2.6 signal-anim-duration)
(reset! clear-timeout ;; TODO: Research if we can implement this with withSequence method
(js/setTimeout ;; from Reanimated 2
;; GitHub issue [#14561]:
;; https://github.com/status-im/status-mobile/issues/14561
(reset! clear-timeout
(js/setTimeout
(fn []
(helpers/set-value scale scale-to-each)
(helpers/animate-linear-with-delay-loop scale
scale-to-total
signal-anim-duration-2
0))
signal-anim-duration))))
stop-animation (rn/use-callback
(fn []
(helpers/set-value opacity 0)
(reanimated/cancel-animation scale)
(helpers/set-value scale 1)
(when @clear-timeout (js/clearTimeout @clear-timeout))))
start-y-animation (rn/use-callback
(fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
[complete-animation])
reset-y-animation (rn/use-callback
(fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-linear icon-opacity 1 500)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500)))
start-x-animation (rn/use-callback
(fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-x -64 250)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66)
(helpers/animate-linear red-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
[complete-animation])
reset-x-animation (rn/use-callback
(fn []
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear red-overlay-opacity 0 100)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500)))
start-x-y-animation (rn/use-callback
(fn [] (fn []
(helpers/set-value scale scale-to-each) (reset! record-button-at-initial-position? false)
(helpers/animate-linear-with-delay-loop scale (reset! record-button-is-animating? true)
scale-to-total (helpers/animate-easing translate-y -44 200)
signal-anim-duration-2 (helpers/animate-easing translate-x -44 200)
0)) (helpers/animate-linear-with-delay icon-opacity 0 33.33 33.33)
signal-anim-duration))) (helpers/animate-linear gray-overlay-opacity 1 33.33)
stop-animation (fn [] (js/setTimeout (fn []
(helpers/set-value opacity 0) (reset! record-button-is-animating? false)
(reanimated/cancel-animation scale) (when-not @touch-active? (complete-animation)))
(helpers/set-value scale 1) 200))
(when @clear-timeout (js/clearTimeout @clear-timeout))) [complete-animation])
start-y-animation (fn [] reset-x-y-animation (rn/use-callback
(reset! record-button-at-initial-position? false) (fn []
(reset! record-button-is-animating? true) (helpers/animate-easing translate-y 0 300)
(helpers/animate-easing translate-y -64 250) (helpers/animate-easing translate-x 0 300)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66) (helpers/animate-linear icon-opacity 1 500)
(js/setTimeout (fn [] (helpers/animate-linear gray-overlay-opacity 0 800)
(reset! record-button-is-animating? false) (js/setTimeout (fn []
(when-not @touch-active? (complete-animation))) (reset! record-button-at-initial-position? true))
250)) 800)))]
reset-y-animation (fn [] (rn/use-effect (fn []
(helpers/animate-easing translate-y 0 300) (cond
(helpers/animate-linear icon-opacity 1 500) recording?
(js/setTimeout (fn [] (start-animation)
(reset! record-button-at-initial-position? true)) (not ready-to-lock?)
500)) (stop-animation)))
start-x-animation (fn [] [recording?])
(reset! record-button-at-initial-position? false) (rn/use-effect (fn []
(reset! record-button-is-animating? true) (if ready-to-lock?
(helpers/animate-easing translate-x -64 250) (start-x-y-animation)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 76.66) (reset-x-y-animation)))
(helpers/animate-linear red-overlay-opacity 1 33.33) [ready-to-lock?])
(js/setTimeout (fn [] (rn/use-effect (fn []
(reset! record-button-is-animating? false) (if ready-to-send?
(when-not @touch-active? (complete-animation))) (start-y-animation)
250)) (reset-y-animation)))
reset-x-animation (fn [] [ready-to-send?])
(helpers/animate-easing translate-x 0 300) (rn/use-effect (fn []
(helpers/animate-linear icon-opacity 1 500) (if ready-to-delete?
(helpers/animate-linear red-overlay-opacity 0 100) (start-x-animation)
(js/setTimeout (fn [] (reset-x-animation)))
(reset! record-button-at-initial-position? true)) [ready-to-delete?])
500))
start-x-y-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(helpers/animate-easing translate-y -44 200)
(helpers/animate-easing translate-x -44 200)
(helpers/animate-linear-with-delay icon-opacity 0 33.33 33.33)
(helpers/animate-linear gray-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
200))
reset-x-y-animation (fn []
(helpers/animate-easing translate-y 0 300)
(helpers/animate-easing translate-x 0 300)
(helpers/animate-linear icon-opacity 1 500)
(helpers/animate-linear gray-overlay-opacity 0 800)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
800))]
(use-effect (fn []
(cond
@recording?
(start-animation)
(not @ready-to-lock?)
(stop-animation)))
[@recording?])
(use-effect (fn []
(if @ready-to-lock?
(start-x-y-animation)
(reset-x-y-animation)))
[@ready-to-lock?])
(use-effect (fn []
(if @ready-to-send?
(start-y-animation)
(reset-y-animation)))
[@ready-to-send?])
(use-effect (fn []
(if @ready-to-delete?
(start-x-animation)
(reset-x-animation)))
[@ready-to-delete?])
[reanimated/view [reanimated/view
{:style (style/record-button-big-container translate-x translate-y opacity) {:style (style/record-button-big-container translate-x translate-y opacity)
:pointer-events :none} :pointer-events :none}
@ -197,6 +215,6 @@
[reanimated/view {:style (style/record-button-big-red-overlay red-overlay-opacity)}] [reanimated/view {:style (style/record-button-big-red-overlay red-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-gray-overlay gray-overlay-opacity)}] [reanimated/view {:style (style/record-button-big-gray-overlay gray-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-icon-container icon-opacity)} [reanimated/view {:style (style/record-button-big-icon-container icon-opacity)}
(if @locked? (if locked?
[rn/view {:style style/stop-icon}] [rn/view {:style style/stop-icon}]
[icons/icon :i/audio {:color icon-color}])]]])) [icons/icon :i/audio {:color icon-color}])]]]))

View File

@ -4,10 +4,10 @@
[quo.components.record-audio.record-audio.helpers :as helpers] [quo.components.record-audio.record-audio.helpers :as helpers]
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[react-native.core :refer [use-effect]] [react-native.core :as rn]
[react-native.reanimated :as reanimated])) [react-native.reanimated :as reanimated]))
(defn f-send-button (defn send-button
[recording? ready-to-send? reviewing-audio? force-show-controls?] [recording? ready-to-send? reviewing-audio? force-show-controls?]
(let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0)) (let [opacity (reanimated/use-shared-value (if force-show-controls? 1 0))
translate-y (reanimated/use-shared-value (if force-show-controls? 76 20)) translate-y (reanimated/use-shared-value (if force-show-controls? 76 20))
@ -16,65 +16,71 @@
height (reanimated/use-shared-value 24) height (reanimated/use-shared-value 24)
border-radius-first-half (reanimated/use-shared-value 16) border-radius-first-half (reanimated/use-shared-value 16)
border-radius-second-half (reanimated/use-shared-value 8) border-radius-second-half (reanimated/use-shared-value 8)
start-y-animation (fn [] start-y-animation (rn/use-callback
(helpers/animate-linear-with-delay translate-y 12 50 133.33) (fn []
(helpers/animate-easing-with-delay connector-opacity 1 0 93.33) (helpers/animate-linear-with-delay translate-y 12 50 133.33)
(helpers/animate-easing-with-delay width 56 83.33 80) (helpers/animate-easing-with-delay connector-opacity 1 0 93.33)
(helpers/animate-easing-with-delay height 56 83.33 80) (helpers/animate-easing-with-delay width 56 83.33 80)
(helpers/animate-easing-with-delay border-radius-first-half (helpers/animate-easing-with-delay height 56 83.33 80)
28 (helpers/animate-easing-with-delay border-radius-first-half
83.33 28
80) 83.33
(helpers/animate-easing-with-delay border-radius-second-half 80)
28 (helpers/animate-easing-with-delay border-radius-second-half
83.33 28
80)) 83.33
reset-y-animation (fn [] 80)))
(helpers/animate-linear translate-y 0 100) reset-y-animation (rn/use-callback
(helpers/set-value connector-opacity 0) (fn []
(helpers/set-value width 12) (helpers/animate-linear translate-y 0 100)
(helpers/set-value height 24) (helpers/set-value connector-opacity 0)
(helpers/set-value border-radius-first-half 16) (helpers/set-value width 12)
(helpers/set-value border-radius-second-half 8)) (helpers/set-value height 24)
fade-in-animation (fn [] (helpers/set-value border-radius-first-half 16)
(helpers/animate-linear translate-y 0 200) (helpers/set-value border-radius-second-half 8)))
(helpers/animate-linear opacity 1 200)) fade-in-animation (rn/use-callback
fade-out-animation (fn [] (fn []
(when-not force-show-controls? (helpers/animate-linear translate-y 0 200)
(helpers/animate-linear (helpers/animate-linear opacity 1 200)))
translate-y fade-out-animation (rn/use-callback
(if @reviewing-audio? 76 20) (fn []
200)) (when-not force-show-controls?
(when-not @reviewing-audio? (helpers/animate-linear
(helpers/animate-linear opacity 0 200)) translate-y
(helpers/set-value connector-opacity 0) (if reviewing-audio? 76 20)
(helpers/set-value width 24) 200))
(helpers/set-value height 12) (when-not reviewing-audio?
(helpers/set-value border-radius-first-half 8) (helpers/animate-linear opacity 0 200))
(helpers/set-value border-radius-second-half 16)) (helpers/set-value connector-opacity 0)
fade-out-reset-animation (fn [] (helpers/set-value width 24)
(helpers/animate-linear opacity 0 200) (helpers/set-value height 12)
(helpers/animate-linear-with-delay translate-y 20 0 200) (helpers/set-value border-radius-first-half 8)
(helpers/set-value connector-opacity 0) (helpers/set-value border-radius-second-half 16))
(helpers/set-value width 24) [reviewing-audio? force-show-controls?])
(helpers/set-value height 12) fade-out-reset-animation (rn/use-callback
(helpers/set-value border-radius-first-half 8) (fn []
(helpers/set-value border-radius-second-half 16))] (helpers/animate-linear opacity 0 200)
(use-effect (fn [] (helpers/animate-linear-with-delay translate-y 20 0 200)
(if @recording? (helpers/set-value connector-opacity 0)
(fade-in-animation) (helpers/set-value width 24)
(fade-out-animation))) (helpers/set-value height 12)
[@recording?]) (helpers/set-value border-radius-first-half 8)
(use-effect (fn [] (helpers/set-value border-radius-second-half 16)))]
(when-not @reviewing-audio? (rn/use-effect (fn []
(fade-out-reset-animation))) (if recording?
[@reviewing-audio?]) (fade-in-animation)
(use-effect (fn [] (fade-out-animation)))
(cond [recording?])
@ready-to-send? (rn/use-effect (fn []
(start-y-animation) (when-not reviewing-audio?
@recording? (reset-y-animation))) (fade-out-reset-animation)))
[@ready-to-send?]) [reviewing-audio?])
(rn/use-effect (fn []
(cond
ready-to-send?
(start-y-animation)
recording? (reset-y-animation)))
[ready-to-send?])
[:<> [:<>
[reanimated/view {:style (style/send-button-container opacity)} [reanimated/view {:style (style/send-button-container opacity)}
[reanimated/view [reanimated/view

View File

@ -2,7 +2,6 @@
(:require (:require
[quo.components.record-audio.record-audio.view :as record-audio] [quo.components.record-audio.record-audio.view :as record-audio]
[react-native.audio-toolkit :as audio] [react-native.audio-toolkit :as audio]
[reagent.core :as reagent]
[test-helpers.component :as h] [test-helpers.component :as h]
[utils.datetime :as datetime])) [utils.datetime :as datetime]))
@ -11,8 +10,10 @@
(h/test "renders record-audio" (h/test "renders record-audio"
(h/render [record-audio/record-audio]) (h/render [record-audio/record-audio])
(-> (h/expect (h/get-by-test-id "record-audio")) (-> (h/wait-for #(h/get-by-test-id "record-audio"))
(.toBeTruthy))) (.then (fn []
(-> (h/expect (h/get-by-test-id "record-audio"))
(.toBeTruthy))))))
(h/test "record-audio on-start-recording works" (h/test "record-audio on-start-recording works"
(let [event (js/jest.fn)] (let [event (js/jest.fn)]
@ -31,7 +32,7 @@
(h/test "record-audio on-reviewing-audio works" (h/test "record-audio on-reviewing-audio works"
(let [event (js/jest.fn) (let [event (js/jest.fn)
on-meter (reagent/atom nil)] on-meter (atom nil)]
(h/render [record-audio/record-audio (h/render [record-audio/record-audio
{:on-reviewing-audio event {:on-reviewing-audio event
:record-audio-permission-granted true}]) :record-audio-permission-granted true}])
@ -39,7 +40,7 @@
(reset! on-meter on-meter-fn)) (reset! on-meter on-meter-fn))
audio/start-recording (fn [_ on-start _] audio/start-recording (fn [_ on-start _]
(on-start) (on-start)
(js/setInterval #(@on-meter) 100)) (js/setInterval @on-meter 100))
audio/get-recorder-file-path (fn [] "file-path")] audio/get-recorder-file-path (fn [] "file-path")]
(h/fire-event (h/fire-event
:on-start-should-set-responder :on-start-should-set-responder
@ -48,6 +49,7 @@
:locationY 70 :locationY 70
:timestamp 0 :timestamp 0
:identifier 0}}) :identifier 0}})
(with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))] (with-redefs [datetime/timestamp (fn [] (+ (.now js/Date) 1000))]
(h/advance-timers-by-time 100) (h/advance-timers-by-time 100)
(h/fire-event (h/fire-event
@ -63,7 +65,7 @@
(h/test "record-audio on-send works after reviewing audio" (h/test "record-audio on-send works after reviewing audio"
(let [event (js/jest.fn) (let [event (js/jest.fn)
on-meter (reagent/atom nil)] on-meter (atom nil)]
(h/render [record-audio/record-audio (h/render [record-audio/record-audio
{:on-send event {:on-send event
:record-audio-permission-granted true}]) :record-audio-permission-granted true}])
@ -114,14 +116,15 @@
(h/test "record-audio on-send works after sliding to the send button" (h/test "record-audio on-send works after sliding to the send button"
(let [event (js/jest.fn) (let [event (js/jest.fn)
on-meter (reagent/atom nil) on-meter (atom nil)
last-now-ms (atom nil) last-now-ms (atom nil)
duration-ms (atom nil)] duration-ms (atom nil)]
(h/render [record-audio/record-audio (h/render [record-audio/record-audio
{:on-send event {:on-send event
:record-audio-permission-granted true}]) :record-audio-permission-granted true}])
(with-redefs [audio/new-recorder (fn [_ on-meter-fn _] (with-redefs [audio/new-recorder (fn [_ on-meter-fn _]
(reset! on-meter on-meter-fn)) (reset! on-meter on-meter-fn)
#js {:destroy #()})
audio/start-recording (fn [_ on-start _] audio/start-recording (fn [_ on-start _]
(on-start) (on-start)
(js/setInterval #(@on-meter) 100)) (js/setInterval #(@on-meter) 100))
@ -170,15 +173,16 @@
(h/test "record-audio on-cancel works after reviewing audio" (h/test "record-audio on-cancel works after reviewing audio"
(let [event (js/jest.fn) (let [event (js/jest.fn)
on-meter (reagent/atom nil)] on-meter (atom nil)]
(h/render [record-audio/record-audio (h/render [record-audio/record-audio
{:on-cancel event {:on-cancel event
:record-audio-permission-granted true}]) :record-audio-permission-granted true}])
(with-redefs [audio/new-recorder (fn [_ on-meter-fn _] (with-redefs [audio/new-recorder (fn [_ on-meter-fn _]
(reset! on-meter on-meter-fn)) (reset! on-meter on-meter-fn)
#js {:destroy #()})
audio/start-recording (fn [_ on-start _] audio/start-recording (fn [_ on-start _]
(on-start) (on-start)
(js/setInterval #(@on-meter) 100))] (js/setInterval @on-meter 100))]
(h/fire-event (h/fire-event
:on-start-should-set-responder :on-start-should-set-responder
(h/get-by-test-id "record-audio") (h/get-by-test-id "record-audio")
@ -195,6 +199,7 @@
:locationY 70 :locationY 70
:timestamp 200 :timestamp 200
:identifier 0}}) :identifier 0}})
(h/advance-timers-by-time 250)
(h/fire-event (h/fire-event
:on-responder-release :on-responder-release
(h/get-by-test-id "record-audio") (h/get-by-test-id "record-audio")
@ -202,13 +207,12 @@
:locationY 80 :locationY 80
:timestamp 200 :timestamp 200
:identifier 0}}) :identifier 0}})
(h/advance-timers-by-time 250)
(-> (js/expect event) (-> (js/expect event)
(.toHaveBeenCalledTimes 1)))))) (.toHaveBeenCalledTimes 1))))))
(h/test "record-audio on-cancel works after sliding to the cancel button" (h/test "record-audio on-cancel works after sliding to the cancel button"
(let [event (js/jest.fn) (let [event (js/jest.fn)
on-meter (reagent/atom nil)] on-meter (atom nil)]
(h/render [record-audio/record-audio (h/render [record-audio/record-audio
{:on-cancel event {:on-cancel event
:record-audio-permission-granted true}]) :record-audio-permission-granted true}])

View File

@ -0,0 +1,81 @@
(ns quo.components.record-audio.record-audio.constants
(:require [react-native.audio-toolkit :as audio]
[react-native.platform :as platform]
[utils.datetime :as datetime]))
(def min-audio-duration-ms 1000)
(def max-audio-duration-ms (if platform/ios? 120800 120500))
(def metering-interval 25)
(def base-filename "am")
(def default-format ".aac")
(def min-touch-duration 150)
(def record-button-area-big
{:width 56
:height 56
:x 64
:y 64})
(def record-button-area
{:width 48
:height 48
:x 68
:y 68})
(defn delete-button-area
[{:keys [active? reviewing-audio?]}]
{:width (cond
active? 72
reviewing-audio? 32
:else 82)
:height (if reviewing-audio? 32 56)
:x (cond
active? -16
reviewing-audio? 36
:else -32)
:y (cond
active? 64
reviewing-audio? 76
:else 70)})
(defn lock-button-area
[{:keys [active?]}]
{:width (if active? 72 100)
:height (if active? 72 102)
:x -32
:y -32})
(defn send-button-area
[{:keys [active? reviewing-audio?]}]
{:width (if reviewing-audio? 32 56)
:height (cond
active? 72
reviewing-audio? 47
:else 92)
:x (if reviewing-audio? 76 32)
:y (cond
active? -16
reviewing-audio? 76
:else -32)})
(defn touch-inside-area?
[{:keys [location-x location-y ignore-min-y? ignore-max-y? ignore-min-x? ignore-max-x?]}
{:keys [width height x y]}]
(let [max-x (+ x width)
max-y (+ y height)]
(and
(and
(or ignore-min-x? (>= location-x x))
(or ignore-max-x? (<= location-x max-x)))
(and
(or ignore-min-y? (>= location-y y))
(or ignore-max-y? (<= location-y max-y))))))
(def rec-options
(merge
audio/default-recorder-options
{:filename (str base-filename
(datetime/timestamp)
default-format)
:meteringInterval metering-interval}))

View File

@ -0,0 +1,345 @@
(ns quo.components.record-audio.record-audio.handlers
(:require [oops.core :as oops]
[quo.components.record-audio.record-audio.constants :as record-audio.constants]
[react-native.audio-toolkit :as audio]
[taoensso.timbre :as log]
[utils.datetime :as datetime]))
(def get-reload-player
(memoize
(fn
[[player-ref destroy-player set-playing-audio playing-timer set-audio-current-time-ms
set-seeking-audio]]
(fn [audio-file]
(when @player-ref
(destroy-player))
(reset! player-ref
(audio/new-player
(or audio-file (:filename record-audio.constants/rec-options))
{:autoDestroy false
:continuesToPlayInBackground false
:category audio/PLAYBACK}
(fn []
(set-playing-audio false)
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil)
(set-audio-current-time-ms 0)
(set-seeking-audio false)))))
(audio/prepare-player
@player-ref
#(log/debug "[record-audio] prepare player - success")
#(log/error "[record-audio] prepare player - error: " %))))))
(def get-recorder-on-meter
(memoize
(fn
[[recording-start-ms set-recording-length-ms reached-max-duration? locked? set-locked
set-reviewing-audio
idle? set-recording
set-ready-to-lock set-ready-to-send set-ready-to-delete recorder-ref output-file reload-player
on-reviewing-audio]]
(fn []
(when @recording-start-ms
(let [now-ms (datetime/timestamp)
recording-duration (- now-ms @recording-start-ms)]
(set-recording-length-ms recording-duration)
(when (>= recording-duration record-audio.constants/max-audio-duration-ms)
(reset! reached-max-duration? (not locked?))
(set-reviewing-audio true)
(reset! idle? false)
(set-locked false)
(set-recording false)
(set-ready-to-lock false)
(set-ready-to-send false)
(set-ready-to-delete false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(reload-player nil)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(log/debug "[record-audio] new recorder - on meter")))))))
(def get-reload-recorder
(memoize
(fn
[[recorder-ref recorder-on-meter]]
(fn []
(when @recorder-ref
(audio/destroy-recorder @recorder-ref)
(reset! recorder-ref nil))
(reset! recorder-ref (audio/new-recorder
record-audio.constants/rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended")))))))
(def get-on-start-should-set-responder
(memoize
(fn
[[locked? idle? disabled? recorder-on-meter touch-timestamp touch-identifier
reviewing-audio? record-audio-permission-granted set-recording set-playing-audio output-file
recorder-ref recording-start-ms set-audio-current-time-ms on-start-recording
on-request-record-audio-permission touch-active?]]
(fn [e]
(when-not (or locked? @idle? (nil? e) @disabled?)
(let [pressed-record-button? (record-audio.constants/touch-inside-area?
{:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-audio.constants/record-button-area)
new-recorder (audio/new-recorder
record-audio.constants/rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))]
(reset! touch-timestamp (oops/oget e "nativeEvent.timestamp"))
(reset! touch-identifier (oops/oget e "nativeEvent.identifier"))
(when-not reviewing-audio?
(if record-audio-permission-granted
(do
(when (not @idle?)
(set-recording pressed-record-button?))
(when pressed-record-button?
(set-playing-audio false)
(reset! output-file nil)
(reset! recorder-ref new-recorder)
(audio/start-recording
new-recorder
(fn []
(reset! recording-start-ms (datetime/timestamp))
(set-audio-current-time-ms 0)
(log/debug "[record-audio] start recording - success"))
#(log/error "[record-audio] start recording - error: " %))
(when on-start-recording
(on-start-recording))))
(when on-request-record-audio-permission
(on-request-record-audio-permission))))
(when record-audio-permission-granted
(reset! touch-active? true))))
(and (not @idle?) (not @disabled?))))))
(def get-on-responder-move
(memoize
(fn
[[locked? ready-to-send? set-ready-to-send ready-to-delete? set-ready-to-delete ready-to-lock?
set-ready-to-lock touch-identifier
record-button-at-initial-position? recording?]]
(fn [^js e]
(when-not locked?
(let [location-x (oops/oget e "nativeEvent.locationX")
location-y (oops/oget e "nativeEvent.locationY")
page-x (oops/oget e "nativeEvent.pageX")
page-y (oops/oget e "nativeEvent.pageY")
identifier (oops/oget e "nativeEvent.identifier")
moved-to-send-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? true
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? true}
(record-audio.constants/send-button-area
{:active? ready-to-send?
:reviewing-audio? false}))
moved-to-delete-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? true
:ignore-min-x? true
:ignore-max-x? false}
(record-audio.constants/delete-button-area
{:active? ready-to-delete?
:reviewing-audio? false}))
moved-to-lock-button? (record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
(record-audio.constants/lock-button-area {:active?
ready-to-lock?}))
moved-to-record-button? (and
(record-audio.constants/touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-audio.constants/record-button-area-big)
(not= location-x page-x)
(not= location-y page-y))]
(when (= identifier @touch-identifier)
(cond
(and
(or
(and moved-to-record-button? ready-to-lock?)
(and (not locked?) moved-to-lock-button? @record-button-at-initial-position?))
(not ready-to-delete?)
(not ready-to-send?)
recording?)
(set-ready-to-lock moved-to-lock-button?)
(and
(or
(and moved-to-record-button? ready-to-delete?)
(and moved-to-delete-button? @record-button-at-initial-position?))
(not ready-to-lock?)
(not ready-to-send?)
recording?)
(set-ready-to-delete moved-to-delete-button?)
(and
(or
(and moved-to-record-button? ready-to-send?)
(and moved-to-send-button? @record-button-at-initial-position?))
(not ready-to-lock?)
(not ready-to-delete?)
recording?)
(set-ready-to-send moved-to-send-button?)))))))))
(def get-on-responder-release
(memoize
(fn
[[idle? reached-max-duration? touch-timestamp recording-length-ms set-recording-length-ms
reviewing-audio? set-reviewing-audio
set-audio-current-time-ms set-force-show-controls on-send output-file player-ref
destroy-player on-cancel record-button-is-animating? ready-to-lock? set-ready-to-lock
locked? set-locked on-lock ready-to-delete? set-ready-to-delete ready-to-send? set-ready-to-send
disabled? on-reviewing-audio recorder-ref set-recording reload-player recording-start-ms
touch-active?]]
(fn [^js e]
(when (and
(not @idle?)
(not @reached-max-duration?))
(let [touch-area {:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
on-record-button? (record-audio.constants/touch-inside-area?
touch-area
record-audio.constants/record-button-area-big)
on-send-button? (record-audio.constants/touch-inside-area?
touch-area
(record-audio.constants/send-button-area
{:active? false
:reviewing-audio? true}))
on-delete-button? (record-audio.constants/touch-inside-area?
touch-area
(record-audio.constants/delete-button-area
{:active? false
:reviewing-audio? true}))
release-touch-timestamp (oops/oget e "nativeEvent.timestamp")
touch-timestamp-diff (- release-touch-timestamp @touch-timestamp)
audio-length recording-length-ms]
(cond
(and reviewing-audio? on-send-button?)
(do
(set-reviewing-audio false)
(set-audio-current-time-ms 0)
(set-force-show-controls false)
(when on-send
(on-send {:file-path @output-file
:duration (min record-audio.constants/max-audio-duration-ms
(int (audio/get-player-duration @player-ref)))}))
(when @player-ref
(audio/stop-playing
@player-ref
(fn []
(destroy-player)
(log/debug "[record-audio] stop playing - success"))
#(log/error "[record-audio] stop playing - error: " %))))
(and reviewing-audio? on-delete-button?)
(do
(set-reviewing-audio false)
(set-audio-current-time-ms 0)
(set-force-show-controls false)
(destroy-player)
(when on-cancel
(on-cancel)))
(and ready-to-lock? (not @record-button-is-animating?))
(do
(set-locked true)
(set-ready-to-lock false)
(when on-lock
(on-lock)))
(and (not reviewing-audio?)
(or on-record-button?
(and (not ready-to-delete?)
(not ready-to-lock?)
(not ready-to-send?))))
(do
(reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration))
(js/setTimeout
(fn []
(if (>= recording-length-ms record-audio.constants/min-audio-duration-ms)
(do (set-reviewing-audio true)
(reset! idle? false)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(do (when on-cancel
(on-cancel))
(reset! idle? true)))
(set-locked false)
(set-recording false)
(set-ready-to-lock false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(when (>= audio-length record-audio.constants/min-audio-duration-ms)
(reload-player nil))
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false))
(if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250)))
(and (not locked?) (not reviewing-audio?) (not @record-button-is-animating?))
(do
(reset! disabled? (<= touch-timestamp-diff record-audio.constants/min-touch-duration))
(js/setTimeout
(fn []
(audio/stop-recording
@recorder-ref
(fn []
(cond
ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration recording-length-ms}))
ready-to-delete?
(when on-cancel
(on-cancel)))
(set-recording false)
(set-ready-to-send false)
(set-ready-to-delete false)
(set-ready-to-lock false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(set-recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %)))
(if (> touch-timestamp-diff record-audio.constants/min-touch-duration) 0 250)))))
(reset! touch-active? false))
(when @reached-max-duration?
(reset! reached-max-duration? false))
(reset! touch-timestamp nil)))))

View File

@ -2,7 +2,6 @@
(:require (:require
[clojure.string :as string] [clojure.string :as string]
[goog.string :as gstring] [goog.string :as gstring]
[oops.core :as oops]
[quo.components.icon :as icons] [quo.components.icon :as icons]
[quo.components.markdown.text :as text] [quo.components.markdown.text :as text]
[quo.components.record-audio.record-audio.buttons.delete-button :as delete-button] [quo.components.record-audio.record-audio.buttons.delete-button :as delete-button]
@ -10,93 +9,23 @@
[quo.components.record-audio.record-audio.buttons.record-button :as record-button] [quo.components.record-audio.record-audio.buttons.record-button :as record-button]
[quo.components.record-audio.record-audio.buttons.record-button-big :as record-button-big] [quo.components.record-audio.record-audio.buttons.record-button-big :as record-button-big]
[quo.components.record-audio.record-audio.buttons.send-button :as send-button] [quo.components.record-audio.record-audio.buttons.send-button :as send-button]
[quo.components.record-audio.record-audio.constants :as record-audio.constants]
[quo.components.record-audio.record-audio.handlers :as handlers]
[quo.components.record-audio.record-audio.style :as style] [quo.components.record-audio.record-audio.style :as style]
[quo.components.record-audio.soundtrack.view :as soundtrack] [quo.components.record-audio.soundtrack.view :as soundtrack]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme :as quo.theme]
[react-native.audio-toolkit :as audio] [react-native.audio-toolkit :as audio]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.platform :as platform]
[reagent.core :as reagent]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[utils.datetime :as datetime])) [utils.datetime :as datetime]))
(def ^:private min-audio-duration-ms 1000) (defn- recording-bar
(def ^:private max-audio-duration-ms (if platform/ios? 120800 120500))
(def ^:private metering-interval 25)
(def ^:private base-filename "am")
(def ^:private default-format ".aac")
(def min-touch-duration 150)
(def ^:private record-button-area-big
{:width 56
:height 56
:x 64
:y 64})
(def ^:private record-button-area
{:width 48
:height 48
:x 68
:y 68})
(defn- delete-button-area
[{:keys [active? reviewing-audio?]}]
{:width (cond
active? 72
reviewing-audio? 32
:else 82)
:height (if reviewing-audio? 32 56)
:x (cond
active? -16
reviewing-audio? 36
:else -32)
:y (cond
active? 64
reviewing-audio? 76
:else 70)})
(defn- lock-button-area
[{:keys [active?]}]
{:width (if active? 72 100)
:height (if active? 72 102)
:x -32
:y -32})
(defn- send-button-area
[{:keys [active? reviewing-audio?]}]
{:width (if reviewing-audio? 32 56)
:height (cond
active? 72
reviewing-audio? 47
:else 92)
:x (if reviewing-audio? 76 32)
:y (cond
active? -16
reviewing-audio? 76
:else -32)})
(defn touch-inside-area?
[{:keys [location-x location-y ignore-min-y? ignore-max-y? ignore-min-x? ignore-max-x?]}
{:keys [width height x y]}]
(let [max-x (+ x width)
max-y (+ y height)]
(and
(and
(or ignore-min-x? (>= location-x x))
(or ignore-max-x? (<= location-x max-x)))
(and
(or ignore-min-y? (>= location-y y))
(or ignore-max-y? (<= location-y max-y))))))
(defn- f-recording-bar
[recording-length-ms ready-to-delete?] [recording-length-ms ready-to-delete?]
(let [fill-percentage (/ (* recording-length-ms 100) max-audio-duration-ms)] (let [fill-percentage (/ (* recording-length-ms 100) record-audio.constants/max-audio-duration-ms)]
[rn/view {:style (style/recording-bar-container)} [rn/view {:style (style/recording-bar-container)}
[rn/view {:style (style/recording-bar fill-percentage ready-to-delete?)}]])) [rn/view {:style (style/recording-bar fill-percentage ready-to-delete?)}]]))
(defn- f-time-counter (defn- time-counter
[recording? recording-length-ms ready-to-delete? reviewing-audio? audio-current-time-ms] [recording? recording-length-ms ready-to-delete? reviewing-audio? audio-current-time-ms]
(let [s (quot (if recording? recording-length-ms audio-current-time-ms) 1000) (let [s (quot (if recording? recording-length-ms audio-current-time-ms) 1000)
time-str (gstring/format "%02d:%02d" (quot s 60) (mod s 60))] time-str (gstring/format "%02d:%02d" (quot s 60) (mod s 60))]
@ -111,10 +40,12 @@
{:style (style/timer-text)})) {:style (style/timer-text)}))
time-str]])) time-str]]))
(defn- f-play-button (defn- play-button
[playing-audio? player-ref playing-timer audio-current-time-ms seeking-audio? max-duration-ms] [playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms seeking-audio?
set-seeking-audio
max-duration-ms]
(let [on-play (fn [] (let [on-play (fn []
(reset! playing-audio? true) (set-playing-audio true)
(reset! playing-timer (reset! playing-timer
(js/setInterval (js/setInterval
(fn [] (fn []
@ -122,24 +53,24 @@
player-state (audio/get-state @player-ref) player-state (audio/get-state @player-ref)
playing? (= player-state audio/PLAYING)] playing? (= player-state audio/PLAYING)]
(when (and playing? (when (and playing?
(not @seeking-audio?) (not seeking-audio?)
(> current-time 0) (> current-time 0)
(< current-time max-duration-ms)) (< current-time max-duration-ms))
(reset! audio-current-time-ms current-time)) (set-audio-current-time-ms current-time))
(when (>= current-time max-duration-ms) (when (>= current-time max-duration-ms)
(audio/stop-playing (audio/stop-playing
@player-ref @player-ref
(fn [] (fn []
(reset! playing-audio? false) (set-playing-audio false)
(when @playing-timer (when @playing-timer
(js/clearInterval @playing-timer) (js/clearInterval @playing-timer)
(reset! playing-timer nil) (reset! playing-timer nil)
(reset! audio-current-time-ms 0) (set-audio-current-time-ms 0)
(reset! seeking-audio? false))) (set-seeking-audio false)))
#(log/error "[record-audio] stop player - error: " %))))) #(log/error "[record-audio] stop player - error: " %)))))
100))) 100)))
on-pause (fn [] on-pause (fn []
(reset! playing-audio? false) (set-playing-audio false)
(when @playing-timer (when @playing-timer
(js/clearInterval @playing-timer) (js/clearInterval @playing-timer)
(reset! playing-timer nil)) (reset! playing-timer nil))
@ -154,445 +85,186 @@
{:style (style/play-button) {:style (style/play-button)
:on-press on-press} :on-press on-press}
[icons/icon [icons/icon
(if @playing-audio? :i/pause :i/play) (if playing-audio? :i/pause :i/play)
{:color (colors/theme-colors colors/neutral-100 colors/white)}]])) {:color (colors/theme-colors colors/neutral-100 colors/white)}]]))
(defn- record-audio-internal (defn record-audio
[{:keys [on-init on-start-recording on-send on-cancel on-reviewing-audio [{:keys [on-init on-start-recording on-send on-cancel on-reviewing-audio audio-file on-lock
record-audio-permission-granted max-duration-ms on-check-audio-permissions record-audio-permission-granted
on-request-record-audio-permission on-check-audio-permissions on-request-record-audio-permission]}]
audio-file on-lock max-duration-ms theme]}] (let [;;STATE
[:f> [recording? set-recording] (rn/use-state false)
;; TODO we need to refactor this, and use :f> with defined function, currenly state is reseted each [locked? set-locked] (rn/use-state false)
;; time parent component [ready-to-send? set-ready-to-send] (rn/use-state false)
;; is re-rendered [ready-to-lock? set-ready-to-lock] (rn/use-state false)
(fn [] [ready-to-delete?
(let [recording? (reagent/atom false) set-ready-to-delete] (rn/use-state false)
locked? (reagent/atom false) [reviewing-audio?
ready-to-send? (reagent/atom false) set-reviewing-audio] (rn/use-state (some? audio-file))
ready-to-lock? (reagent/atom false) [playing-audio?
ready-to-delete? (reagent/atom false) set-playing-audio] (rn/use-state false)
reviewing-audio? (reagent/atom (some? audio-file)) [recording-length-ms
playing-audio? (reagent/atom false) set-recording-length-ms] (rn/use-state 0)
recording-length-ms (reagent/atom 0) [audio-current-time-ms
audio-current-time-ms (reagent/atom 0) set-audio-current-time-ms] (rn/use-state 0)
seeking-audio? (reagent/atom false) [seeking-audio? set-seeking-audio] (rn/use-state false)
force-show-controls? (reagent/atom (some? audio-file)) [force-show-controls?
recording-start-ms (atom (datetime/timestamp)) set-force-show-controls] (rn/use-state (some? audio-file))
clear-timeout (atom nil) ;;ATOMS
record-button-at-initial-position? (atom true) recording-start-ms (rn/use-ref-atom (datetime/timestamp))
record-button-is-animating? (atom false) clear-timeout (rn/use-ref-atom nil)
idle? (atom false) record-button-at-initial-position? (rn/use-ref-atom true)
recorder-ref (atom nil) record-button-is-animating? (rn/use-ref-atom false)
player-ref (atom nil) idle? (rn/use-ref-atom false)
touch-active? (atom false) recorder-ref (rn/use-ref-atom nil)
playing-timer (atom nil) player-ref (rn/use-ref-atom nil)
output-file (atom audio-file) touch-active? (rn/use-ref-atom false)
reached-max-duration? (atom false) playing-timer (rn/use-ref-atom nil)
touch-timestamp (atom nil) output-file (rn/use-ref-atom audio-file)
touch-identifier (atom nil) reached-max-duration? (rn/use-ref-atom false)
disabled? (atom false) touch-timestamp (rn/use-ref-atom nil)
app-state-listener (atom nil) touch-identifier (rn/use-ref-atom nil)
rec-options disabled? (rn/use-ref-atom false)
(merge app-state-listener (rn/use-ref-atom nil)
audio/default-recorder-options ;;HANDLERS
{:filename (str base-filename (datetime/timestamp) default-format) destroy-player (rn/use-callback
:meteringInterval metering-interval}) (fn []
destroy-player (audio/destroy-player @player-ref)
(fn [] (reset! player-ref nil)))
(audio/destroy-player @player-ref) reload-player (handlers/get-reload-player
(reset! player-ref nil)) [player-ref destroy-player set-playing-audio playing-timer
reload-player set-audio-current-time-ms set-seeking-audio])
(fn [audio-file] recorder-on-meter (handlers/get-recorder-on-meter
(when @player-ref [recording-start-ms set-recording-length-ms
(destroy-player)) reached-max-duration?
(reset! player-ref locked? set-locked set-reviewing-audio idle? set-recording
(audio/new-player set-ready-to-lock set-ready-to-send set-ready-to-delete
(or audio-file (:filename rec-options)) recorder-ref
{:autoDestroy false output-file reload-player
:continuesToPlayInBackground false on-reviewing-audio])
:category audio/PLAYBACK} reload-recorder (handlers/get-reload-recorder [recorder-ref
(fn [] recorder-on-meter])
(reset! playing-audio? false) on-start-should-set-responder (handlers/get-on-start-should-set-responder
(when @playing-timer [locked? idle? disabled? recorder-on-meter touch-timestamp
(js/clearInterval @playing-timer) touch-identifier
(reset! playing-timer nil) reviewing-audio? record-audio-permission-granted
(reset! audio-current-time-ms 0) set-recording set-playing-audio output-file
(reset! seeking-audio? false))))) recorder-ref recording-start-ms set-audio-current-time-ms
(audio/prepare-player on-start-recording
@player-ref on-request-record-audio-permission touch-active?])
#(log/debug "[record-audio] prepare player - success") on-responder-move (handlers/get-on-responder-move
#(log/error "[record-audio] prepare player - error: " %))) [locked? ready-to-send? set-ready-to-send
destroy-recorder ready-to-delete? set-ready-to-delete
(fn [] ready-to-lock? set-ready-to-lock
(audio/destroy-recorder @recorder-ref) touch-identifier
(reset! recorder-ref nil)) record-button-at-initial-position?
recorder-on-meter recording?])
(fn [] on-responder-release (handlers/get-on-responder-release
(when @recording-start-ms [idle? reached-max-duration? touch-timestamp
(let [now-ms (datetime/timestamp) recording-length-ms set-recording-length-ms
recording-duration (- now-ms reviewing-audio? set-reviewing-audio
@recording-start-ms)] set-audio-current-time-ms set-force-show-controls on-send
(reset! recording-length-ms recording-duration) output-file player-ref
(when (>= recording-duration max-audio-duration-ms) destroy-player on-cancel record-button-is-animating?
(reset! reached-max-duration? (not @locked?)) ready-to-lock? set-ready-to-lock
(reset! reviewing-audio? true) locked? set-locked on-lock ready-to-delete?
(reset! idle? false) set-ready-to-delete ready-to-send? set-ready-to-send
(reset! locked? false) disabled? on-reviewing-audio
(reset! recording? false) recorder-ref set-recording reload-player recording-start-ms
(reset! ready-to-lock? false) touch-active?])]
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path
@recorder-ref))
(reload-player nil)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path
@recorder-ref))))
(log/debug "[record-audio] new recorder - on meter"))))
reload-recorder
(fn []
(when @recorder-ref
(destroy-recorder))
(reset! recorder-ref (audio/new-recorder
rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))))
reset-recorder
(fn []
(reset! recording? false)
(reset! reviewing-audio? false)
(reset! locked? false)
(reset! ready-to-send? false)
(reset! ready-to-lock? false)
(reset! ready-to-delete? false)
(reset! audio-current-time-ms 0)
(reset! recording-length-ms 0)
(reset! seeking-audio? false)
(reset! playing-audio? false)
(reset! touch-active? false)
(reset! reached-max-duration? false)
(reset! output-file nil)
(reset! idle? false)
(reset! record-button-is-animating? false)
(reset! record-button-at-initial-position? true)
(when @clear-timeout
(js/clearTimeout @clear-timeout)
(reset! clear-timeout nil))
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil))
(reload-recorder))
on-start-should-set-responder
(fn [^js e]
(when-not (or @locked? @idle? (nil? e) @disabled?)
(let [pressed-record-button? (touch-inside-area?
{:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-button-area)
new-recorder (audio/new-recorder
rec-options
recorder-on-meter
#(log/debug "[record-audio] new recorder - on ended"))]
(reset! touch-timestamp (oops/oget e "nativeEvent.timestamp"))
(reset! touch-identifier (oops/oget e "nativeEvent.identifier"))
(when-not @reviewing-audio?
(if record-audio-permission-granted
(do
(when (not @idle?)
(reset! recording? pressed-record-button?))
(when pressed-record-button?
(reset! playing-audio? false)
(reset! output-file nil)
(reset! recorder-ref new-recorder)
(audio/start-recording
new-recorder
(fn []
(reset! recording-start-ms (datetime/timestamp))
(reset! audio-current-time-ms 0)
(log/debug "[record-audio] start recording - success"))
#(log/error "[record-audio] start recording - error: " %))
(when on-start-recording
(on-start-recording))))
(when on-request-record-audio-permission
(on-request-record-audio-permission))))
(when record-audio-permission-granted
(reset! touch-active? true))))
(and (not @idle?) (not @disabled?)))
on-responder-move
(fn [^js e]
(when-not @locked?
(let [location-x (oops/oget e "nativeEvent.locationX")
location-y (oops/oget e "nativeEvent.locationY")
page-x (oops/oget e "nativeEvent.pageX")
page-y (oops/oget e "nativeEvent.pageY")
identifier (oops/oget e "nativeEvent.identifier")
moved-to-send-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? true
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? true}
(send-button-area
{:active? @ready-to-send?
:reviewing-audio? false}))
moved-to-delete-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? true
:ignore-min-x? true
:ignore-max-x? false}
(delete-button-area
{:active? @ready-to-delete?
:reviewing-audio? false}))
moved-to-lock-button? (touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
(lock-button-area {:active? @ready-to-lock?}))
moved-to-record-button? (and
(touch-inside-area?
{:location-x location-x
:location-y location-y
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
record-button-area-big)
(not= location-x page-x)
(not= location-y page-y))]
(when (= identifier @touch-identifier)
(cond
(and
(or
(and moved-to-record-button? @ready-to-lock?)
(and (not @locked?) moved-to-lock-button? @record-button-at-initial-position?))
(not @ready-to-delete?)
(not @ready-to-send?)
@recording?)
(reset! ready-to-lock? moved-to-lock-button?)
(and
(or
(and moved-to-record-button? @ready-to-delete?)
(and moved-to-delete-button? @record-button-at-initial-position?))
(not @ready-to-lock?)
(not @ready-to-send?)
@recording?)
(reset! ready-to-delete? moved-to-delete-button?)
(and
(or
(and moved-to-record-button? @ready-to-send?)
(and moved-to-send-button? @record-button-at-initial-position?))
(not @ready-to-lock?)
(not @ready-to-delete?)
@recording?)
(reset! ready-to-send? moved-to-send-button?))))))
on-responder-release
(fn [^js e]
(when (and
(not @idle?)
(not @reached-max-duration?))
(let [touch-area {:location-x (oops/oget e "nativeEvent.locationX")
:location-y (oops/oget e "nativeEvent.locationY")
:ignore-min-y? false
:ignore-max-y? false
:ignore-min-x? false
:ignore-max-x? false}
on-record-button? (touch-inside-area?
touch-area
record-button-area-big)
on-send-button? (touch-inside-area?
touch-area
(send-button-area
{:active? false
:reviewing-audio? true}))
on-delete-button? (touch-inside-area?
touch-area
(delete-button-area
{:active? false
:reviewing-audio? true}))
release-touch-timestamp (oops/oget e "nativeEvent.timestamp")
touch-timestamp-diff (- release-touch-timestamp @touch-timestamp)
audio-length @recording-length-ms]
(cond
(and @reviewing-audio? on-send-button?)
(do
(reset! reviewing-audio? false)
(reset! audio-current-time-ms 0)
(reset! force-show-controls? false)
(when on-send
(on-send {:file-path @output-file
:duration (min max-audio-duration-ms
(int (audio/get-player-duration @player-ref)))}))
(when @player-ref
(audio/stop-playing
@player-ref
(fn []
(destroy-player)
(log/debug "[record-audio] stop playing - success"))
#(log/error "[record-audio] stop playing - error: " %))))
(and @reviewing-audio? on-delete-button?)
(do
(reset! reviewing-audio? false)
(reset! audio-current-time-ms 0)
(reset! force-show-controls? false)
(destroy-player)
(when on-cancel
(on-cancel)))
(and @ready-to-lock? (not @record-button-is-animating?))
(do
(reset! locked? true)
(reset! ready-to-lock? false)
(when on-lock
(on-lock)))
(and (not @reviewing-audio?)
(or on-record-button?
(and (not @ready-to-delete?)
(not @ready-to-lock?)
(not @ready-to-send?))))
(do
(reset! disabled? (<= touch-timestamp-diff min-touch-duration))
(js/setTimeout
(fn []
(if (>= @recording-length-ms min-audio-duration-ms)
(do (reset! reviewing-audio? true)
(reset! idle? false)
(when on-reviewing-audio
(on-reviewing-audio (audio/get-recorder-file-path @recorder-ref))))
(do (when on-cancel
(on-cancel))
(reset! idle? true)))
(reset! locked? false)
(reset! recording? false)
(reset! ready-to-lock? false)
(audio/stop-recording
@recorder-ref
(fn []
(reset! output-file (audio/get-recorder-file-path @recorder-ref))
(when (>= audio-length min-audio-duration-ms)
(reload-player nil))
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false))
(if (> touch-timestamp-diff min-touch-duration) 0 250)))
(and (not @locked?) (not @reviewing-audio?) (not @record-button-is-animating?))
(do
(reset! disabled? (<= touch-timestamp-diff min-touch-duration))
(js/setTimeout
(fn []
(audio/stop-recording
@recorder-ref
(fn []
(cond
@ready-to-send?
(when on-send
(on-send {:file-path (audio/get-recorder-file-path @recorder-ref)
:duration @recording-length-ms}))
@ready-to-delete?
(when on-cancel
(on-cancel)))
(reset! recording? false)
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(reset! ready-to-lock? false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(reset! recording-length-ms 0)
(reset! recording-start-ms nil)
(reset! disabled? false)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %)))
(if (> touch-timestamp-diff min-touch-duration) 0 250)))))
(reset! touch-active? false))
(when @reached-max-duration?
(reset! reached-max-duration? false))
(reset! touch-timestamp nil))]
(fn []
(rn/use-mount (fn []
(when on-check-audio-permissions
(on-check-audio-permissions))
(when on-init
(on-init reset-recorder))
(when audio-file
(let [filename (last (string/split audio-file "/"))]
(reload-player filename)))
(reset! app-state-listener
(.addEventListener rn/app-state
"change"
#(when (= % "background")
(reset! playing-audio? false))))
#(.remove @app-state-listener)))
[rn/view
{:style style/bar-container
:pointer-events :box-none}
(when @reviewing-audio?
[:<>
[:f> f-play-button playing-audio? player-ref playing-timer audio-current-time-ms
seeking-audio? max-duration-ms]
[:f> soundtrack/f-soundtrack
{:audio-current-time-ms audio-current-time-ms
:player-ref @player-ref
:seeking-audio? seeking-audio?
:max-audio-duration-ms max-duration-ms}]])
(when (or @recording? @reviewing-audio?)
[:f> f-time-counter @recording? @recording-length-ms @ready-to-delete? @reviewing-audio?
@audio-current-time-ms])
(when @recording?
[:f> f-recording-bar @recording-length-ms @ready-to-delete?])
[rn/view
{:test-ID "record-audio"
:style style/button-container
:hit-slop {:top -70
:bottom 0
:left 0
:right 0}
:pointer-events :box-only
:on-start-should-set-responder on-start-should-set-responder
:on-responder-move on-responder-move
:on-responder-release on-responder-release}
[:f> delete-button/f-delete-button recording? ready-to-delete? reviewing-audio?
@force-show-controls?]
[:f> lock-button/f-lock-button recording? ready-to-lock? locked?]
[:f> send-button/f-send-button recording? ready-to-send? reviewing-audio?
@force-show-controls?]
[:f> record-button-big/f-record-button-big
{:recording? recording?
:ready-to-send? ready-to-send?
:ready-to-lock? ready-to-lock?
:ready-to-delete? ready-to-delete?
:record-button-is-animating? record-button-is-animating?
:record-button-at-initial-position? record-button-at-initial-position?
:locked? locked?
:reviewing-audio? reviewing-audio?
:recording-length-ms recording-length-ms
:clear-timeout clear-timeout
:touch-active? touch-active?
:recorder-ref recorder-ref
:reload-recorder-fn reload-recorder
:idle? idle?
:on-send on-send
:on-cancel on-cancel
:theme theme}]
[:f> record-button/f-record-button recording? reviewing-audio?]]])))])
(def record-audio (quo.theme/with-theme record-audio-internal)) ;;ON MOUNT
(rn/use-mount
(fn []
(when on-check-audio-permissions (on-check-audio-permissions))
(when on-init
(on-init
(fn reset-recorder []
(set-recording false)
(set-reviewing-audio false)
(set-locked false)
(set-ready-to-send false)
(set-ready-to-lock false)
(set-ready-to-delete false)
(set-audio-current-time-ms 0)
(set-recording-length-ms 0)
(set-seeking-audio false)
(set-playing-audio false)
(reset! touch-active? false)
(reset! reached-max-duration? false)
(reset! output-file nil)
(reset! idle? false)
(reset! record-button-is-animating? false)
(reset! record-button-at-initial-position? true)
(when @clear-timeout
(js/clearTimeout @clear-timeout)
(reset! clear-timeout nil))
(when @playing-timer
(js/clearInterval @playing-timer)
(reset! playing-timer nil))
(reload-recorder))))
(when audio-file
(let [filename (last (string/split audio-file "/"))]
(reload-player filename)))
(reset! app-state-listener
(.addEventListener rn/app-state
"change"
#(when (= % "background")
(set-playing-audio false))))))
;;ON UNMOUNT
(rn/use-unmount #(.remove @app-state-listener))
[rn/view
{:style style/bar-container
:pointer-events :box-none}
(when reviewing-audio?
[:<>
[play-button playing-audio? set-playing-audio player-ref playing-timer set-audio-current-time-ms
seeking-audio? set-seeking-audio max-duration-ms]
[soundtrack/soundtrack
{:audio-current-time-ms audio-current-time-ms
:set-audio-current-time-ms set-audio-current-time-ms
:player-ref @player-ref
:seeking-audio? seeking-audio?
:set-seeking-audio set-seeking-audio
:max-audio-duration-ms max-duration-ms}]])
(when (or recording? reviewing-audio?)
[time-counter recording? recording-length-ms ready-to-delete? reviewing-audio?
audio-current-time-ms])
(when recording?
[recording-bar recording-length-ms ready-to-delete?])
[rn/view
{:test-ID "record-audio"
:style style/button-container
:hit-slop {:top -70 :bottom 0 :left 0 :right 0}
:pointer-events :box-only
:on-start-should-set-responder on-start-should-set-responder
:on-responder-move on-responder-move
:on-responder-release on-responder-release}
[delete-button/delete-button recording? ready-to-delete? reviewing-audio? force-show-controls?]
[lock-button/lock-button recording? ready-to-lock? locked?]
[send-button/send-button recording? ready-to-send? reviewing-audio? force-show-controls?]
[record-button-big/record-button-big
{:recording? recording?
:set-recording set-recording
:ready-to-send? ready-to-send?
:set-ready-to-send set-ready-to-send
:ready-to-lock? ready-to-lock?
:set-ready-to-lock set-ready-to-lock
:ready-to-delete? ready-to-delete?
:set-ready-to-delete set-ready-to-delete
:record-button-is-animating? record-button-is-animating?
:record-button-at-initial-position? record-button-at-initial-position?
:locked? locked?
:set-locked set-locked
:reviewing-audio? reviewing-audio?
:recording-length-ms recording-length-ms
:set-recording-length-ms set-recording-length-ms
:clear-timeout clear-timeout
:touch-active? touch-active?
:recorder-ref recorder-ref
:reload-recorder-fn reload-recorder
:idle? idle?
:on-send on-send
:on-cancel on-cancel}]
[record-button/record-button recording? reviewing-audio?]]]))

View File

@ -2,7 +2,6 @@
(:require (:require
[quo.components.record-audio.soundtrack.view :as soundtrack] [quo.components.record-audio.soundtrack.view :as soundtrack]
[react-native.audio-toolkit :as audio] [react-native.audio-toolkit :as audio]
[reagent.core :as reagent]
[test-helpers.component :as h])) [test-helpers.component :as h]))
(h/describe "soundtrack component" (h/describe "soundtrack component"
@ -10,23 +9,21 @@
(h/test "renders soundtrack" (h/test "renders soundtrack"
(with-redefs [audio/get-player-duration (fn [] 2000)] (with-redefs [audio/get-player-duration (fn [] 2000)]
(let [player-ref (reagent/atom {}) (h/render [soundtrack/soundtrack
audio-current-time-ms (reagent/atom 0)] {:player-ref {}
(h/render [:f> soundtrack/f-soundtrack :audio-current-time-ms 0}])
{:player-ref @player-ref (-> (h/expect (h/get-by-test-id "soundtrack"))
:audio-current-time-ms audio-current-time-ms}]) (.toBeTruthy))))
(-> (h/expect (h/get-by-test-id "soundtrack"))
(.toBeTruthy)))))
(h/test "soundtrack on-sliding-start works" (h/test "soundtrack on-sliding-start works"
(with-redefs [audio/get-player-duration (fn [] 2000)] (with-redefs [audio/get-player-duration (fn [] 2000)]
(let [seeking-audio? (reagent/atom false) (let [seeking-audio? (atom false)]
player-ref (reagent/atom {}) (h/render [soundtrack/soundtrack
audio-current-time-ms (reagent/atom 0)] {:seeking-audio? seeking-audio?
(h/render [:f> soundtrack/f-soundtrack :set-seeking-audio #(reset! seeking-audio? %)
{:seeking-audio? seeking-audio? :player-ref {}
:player-ref @player-ref :audio-current-time-ms 0
:audio-current-time-ms audio-current-time-ms}]) :set-audio-current-time-ms #()}])
(h/fire-event (h/fire-event
:on-sliding-start :on-sliding-start
(h/get-by-test-id "soundtrack")) (h/get-by-test-id "soundtrack"))
@ -36,13 +33,13 @@
(h/test "soundtrack on-sliding-complete works" (h/test "soundtrack on-sliding-complete works"
(with-redefs [audio/get-player-duration (fn [] 2000) (with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)] audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false) (let [seeking-audio? (atom false)]
player-ref (reagent/atom {}) (h/render [soundtrack/soundtrack
audio-current-time-ms (reagent/atom 0)] {:seeking-audio? seeking-audio?
(h/render [:f> soundtrack/f-soundtrack :set-seeking-audio #(reset! seeking-audio? %)
{:seeking-audio? seeking-audio? :player-ref {}
:player-ref @player-ref :audio-current-time-ms 0
:audio-current-time-ms audio-current-time-ms}]) :set-audio-current-time-ms #()}])
(h/fire-event (h/fire-event
:on-sliding-start :on-sliding-start
(h/get-by-test-id "soundtrack")) (h/get-by-test-id "soundtrack"))
@ -58,13 +55,14 @@
(h/test "soundtrack on-value-change when seeking audio works" (h/test "soundtrack on-value-change when seeking audio works"
(with-redefs [audio/get-player-duration (fn [] 2000) (with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)] audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false) (let [seeking-audio? (atom false)
player-ref (reagent/atom {}) audio-current-time-ms (atom 0)]
audio-current-time-ms (reagent/atom 0)] (h/render [soundtrack/soundtrack
(h/render [:f> soundtrack/f-soundtrack {:seeking-audio? seeking-audio?
{:seeking-audio? seeking-audio? :set-seeking-audio #(reset! seeking-audio? %)
:player-ref @player-ref :player-ref {}
:audio-current-time-ms audio-current-time-ms}]) :audio-current-time-ms audio-current-time-ms
:set-audio-current-time-ms #(reset! audio-current-time-ms %)}])
(h/fire-event (h/fire-event
:on-sliding-start :on-sliding-start
(h/get-by-test-id "soundtrack")) (h/get-by-test-id "soundtrack"))

View File

@ -1,42 +1,47 @@
(ns quo.components.record-audio.soundtrack.view (ns quo.components.record-audio.soundtrack.view
(:require (:require
[quo.components.icons.icons :as icons]
[quo.components.record-audio.soundtrack.style :as style] [quo.components.record-audio.soundtrack.style :as style]
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme :as quo.theme] [quo.theme :as quo.theme]
[react-native.audio-toolkit :as audio] [react-native.audio-toolkit :as audio]
[react-native.core :as rn]
[react-native.platform :as platform] [react-native.platform :as platform]
[react-native.slider :as slider] [react-native.slider :as slider]
[taoensso.timbre :as log])) [taoensso.timbre :as log]))
(def ^:private thumb-light (js/require "../resources/images/icons2/12x12/thumb-light.png")) (def ^:private thumb-light (icons/icon-source :thumb-light12))
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png")) (def ^:private thumb-dark (icons/icon-source :thumb-dark12))
(defn f-soundtrack (defn soundtrack
[{:keys [audio-current-time-ms player-ref style seeking-audio? max-audio-duration-ms]}] [{:keys [audio-current-time-ms set-audio-current-time-ms player-ref style
(let [audio-duration-ms (min max-audio-duration-ms (audio/get-player-duration player-ref)) seeking-audio? set-seeking-audio max-audio-duration-ms]}]
theme (quo.theme/use-theme-value)] (let [audio-duration-ms (min max-audio-duration-ms (audio/get-player-duration player-ref))
[:<> theme (quo.theme/use-theme-value)
[slider/slider on-sliding-start (rn/use-callback #(set-seeking-audio true))
{:test-ID "soundtrack" on-sliding-complete (rn/use-callback
:style (merge (fn [seek-time]
(style/player-slider-container) (set-seeking-audio false)
(or style {})) (audio/seek-player
:minimum-value 0 player-ref
:maximum-value audio-duration-ms seek-time
:value @audio-current-time-ms #(log/debug "[record-audio] on seek - seek time: " seek-time)
:on-sliding-start #(reset! seeking-audio? true) #(log/error "[record-audio] on seek - error: " %)))
:on-sliding-complete (fn [seek-time] [player-ref])
(reset! seeking-audio? false) on-value-change (rn/use-callback #(when seeking-audio? (set-audio-current-time-ms %))
(audio/seek-player [seeking-audio?])]
player-ref [slider/slider
seek-time {:test-ID "soundtrack"
#(log/debug "[record-audio] on seek - seek time: " seek-time) :style (merge (style/player-slider-container) style)
#(log/error "[record-audio] on seek - error: " %))) :minimum-value 0
:on-value-change #(when @seeking-audio? :maximum-value audio-duration-ms
(reset! audio-current-time-ms %)) :value audio-current-time-ms
:thumb-image (quo.theme/theme-value thumb-light thumb-dark theme) :on-sliding-start on-sliding-start
:minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60 theme) :on-sliding-complete on-sliding-complete
:maximum-track-tint-color (colors/theme-colors :on-value-change on-value-change
(if platform/ios? colors/neutral-20 colors/neutral-40) :thumb-image (quo.theme/theme-value thumb-light thumb-dark theme)
(if platform/ios? colors/neutral-80 colors/neutral-60) :minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60 theme)
theme)}]])) :maximum-track-tint-color (colors/theme-colors
(if platform/ios? colors/neutral-20 colors/neutral-40)
(if platform/ios? colors/neutral-80 colors/neutral-60)
theme)}]))

View File

@ -357,7 +357,7 @@
;;;; Record Audio ;;;; Record Audio
(def record-audio quo.components.record-audio.record-audio.view/record-audio) (def record-audio quo.components.record-audio.record-audio.view/record-audio)
(def soundtrack quo.components.record-audio.soundtrack.view/f-soundtrack) (def soundtrack quo.components.record-audio.soundtrack.view/soundtrack)
;;;; Selectors ;;;; Selectors
(def disclaimer quo.components.selectors.disclaimer.view/view) (def disclaimer quo.components.selectors.disclaimer.view/view)

View File

@ -38,7 +38,8 @@
(when on-meter (when on-meter
(.on ^js recorder "meter" on-meter)) (.on ^js recorder "meter" on-meter))
(when on-ended (when on-ended
(.on ^js recorder "ended" on-ended)))) (.on ^js recorder "ended" on-ended))
recorder))
(defn new-player (defn new-player
[audio options on-ended] [audio options on-ended]
@ -46,7 +47,8 @@
audio audio
(clj->js options))] (clj->js options))]
(when on-ended (when on-ended
(.on ^js player "ended" on-ended)))) (.on ^js player "ended" on-ended))
player))
(defn prepare-player (defn prepare-player
[player on-prepared on-error] [player on-prepared on-error]
@ -157,20 +159,16 @@
(on-error {:error (.-err err) :message (.-message err)}) (on-error {:error (.-err err) :message (.-message err)})
(on-seek)))))) (on-seek))))))
(defn can-play?
[player]
(and player (.-canPlay ^js player)))
(defn destroy-recorder (defn destroy-recorder
[recorder] [recorder]
(stop-recording recorder (stop-recording recorder
#(when (and recorder (not= (get-state recorder) DESTROYED)) #(when (and recorder (.-destroy ^js recorder) (not= (get-state recorder) DESTROYED))
(.destroy ^js recorder)) (.destroy ^js recorder))
#())) #()))
(defn destroy-player (defn destroy-player
[player] [player]
(stop-playing player (stop-playing player
#(when (and player (not= (get-state player) IDLE)) #(when (and player (.-destroy ^js player) (not= (get-state player) IDLE))
(.destroy ^js player)) (.destroy ^js player))
#())) #()))

View File

@ -210,12 +210,14 @@
:else :i/play-audio) :else :i/play-audio)
{:size 20 {:size 20
:color colors/white}]] :color colors/white}]]
[:f> quo/soundtrack [quo/soundtrack
{:style style/slider-container {:style style/slider-container
:audio-current-time-ms progress :audio-current-time-ms @progress
:player-ref (@active-players player-key) :set-audio-current-time-ms #(reset! progress %)
:seeking-audio? seeking-audio? :player-ref (@active-players player-key)
:max-audio-duration-ms constants/audio-max-duration-ms}] :seeking-audio? @seeking-audio?
:set-seeking-audio? #(reset! seeking-audio? %)
:max-audio-duration-ms constants/audio-max-duration-ms}]
[quo/text [quo/text
{:style style/timestamp {:style style/timestamp
:accessibility-label :audio-duration-label :accessibility-label :audio-duration-label