feat: record audio complete flow
Signed-off-by: Brian Sztamfater <brian@status.im>
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 534 B |
After Width: | Height: | Size: 472 B |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 455 B |
After Width: | Height: | Size: 443 B |
After Width: | Height: | Size: 601 B |
|
@ -0,0 +1,159 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.--tests--.record-audio-component-spec
|
||||||
|
(:require [quo2.components.record-audio.record-audio.view :as record-audio]
|
||||||
|
[status-im.audio.core :as audio]
|
||||||
|
[test-helpers.component :as h]))
|
||||||
|
|
||||||
|
(h/describe "record audio component"
|
||||||
|
(h/before-each
|
||||||
|
(fn []
|
||||||
|
(h/use-fake-timers)))
|
||||||
|
|
||||||
|
(h/after-each
|
||||||
|
(fn []
|
||||||
|
(h/clear-all-timers)
|
||||||
|
(h/use-real-timers)))
|
||||||
|
|
||||||
|
(h/test "renders record-audio"
|
||||||
|
(h/render [record-audio/record-audio])
|
||||||
|
(-> (h/expect (h/get-by-test-id "record-audio"))
|
||||||
|
(.toBeTruthy)))
|
||||||
|
|
||||||
|
(h/test "record-audio on-start-recording works"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-start-recording event}])
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(-> (h/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1))))
|
||||||
|
|
||||||
|
(h/test "record-audio on-reviewing-audio works"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-reviewing-audio event}])
|
||||||
|
(with-redefs [audio/start-recording (fn [_ on-start _]
|
||||||
|
(on-start))]
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/advance-timers-by-time 500)
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(-> (h/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1)))))
|
||||||
|
|
||||||
|
(h/test "record-audio on-send works after reviewing audio"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-send event}])
|
||||||
|
(with-redefs [audio/start-recording (fn [_ on-start _]
|
||||||
|
(on-start))
|
||||||
|
audio/get-recorder-file-path (fn [] "audio-file-path")]
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/advance-timers-by-time 500)
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 80
|
||||||
|
:locationY 80}})
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1))
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledWith "audio-file-path")))))
|
||||||
|
|
||||||
|
(h/test "record-audio on-send works after sliding to the send button"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-send event}])
|
||||||
|
(with-redefs [audio/start-recording (fn [_ on-start _]
|
||||||
|
(on-start))
|
||||||
|
audio/stop-recording (fn [_ on-stop _]
|
||||||
|
(on-stop))
|
||||||
|
audio/get-recorder-file-path (fn [] "audio-file-path")]
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/advance-timers-by-time 500)
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-move
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 80
|
||||||
|
:locationY -30
|
||||||
|
:pageX 80
|
||||||
|
:pageY -30}})
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 40
|
||||||
|
:locationY 80}})
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1))
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledWith "audio-file-path")))))
|
||||||
|
|
||||||
|
(h/test "record-audio on-cancel works after reviewing audio"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-cancel event}])
|
||||||
|
(with-redefs [audio/start-recording (fn [_ on-start _]
|
||||||
|
(on-start))]
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/advance-timers-by-time 500)
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 40
|
||||||
|
:locationY 80}})
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1)))))
|
||||||
|
|
||||||
|
(h/test "cord-audio on-cancel works after sliding to the cancel button"
|
||||||
|
(let [event (js/jest.fn)]
|
||||||
|
(h/render [record-audio/record-audio {:on-cancel event}])
|
||||||
|
(with-redefs [audio/start-recording (fn [_ on-start _]
|
||||||
|
(on-start))
|
||||||
|
audio/stop-recording (fn [_ on-stop _]
|
||||||
|
(on-stop))]
|
||||||
|
(h/fire-event
|
||||||
|
:on-start-should-set-responder
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX 70
|
||||||
|
:locationY 70}})
|
||||||
|
(h/advance-timers-by-time 500)
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-move
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX -30
|
||||||
|
:locationY 80
|
||||||
|
:pageX -30
|
||||||
|
:pageY 80}})
|
||||||
|
(h/fire-event
|
||||||
|
:on-responder-release
|
||||||
|
(h/get-by-test-id "record-audio")
|
||||||
|
{:nativeEvent {:locationX -10
|
||||||
|
:locationY 70}})
|
||||||
|
(-> (js/expect event)
|
||||||
|
(.toHaveBeenCalledTimes 1))))))
|
|
@ -0,0 +1,88 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.buttons.delete-button
|
||||||
|
(:require [quo2.components.icon :as icons]
|
||||||
|
[quo2.components.record-audio.record-audio.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.reanimated :as reanimated]
|
||||||
|
[react-native.core :refer [use-effect]]
|
||||||
|
[quo2.components.record-audio.record-audio.helpers :refer
|
||||||
|
[animate-linear-with-delay
|
||||||
|
animate-easing-with-delay
|
||||||
|
animate-linear
|
||||||
|
set-value]]))
|
||||||
|
|
||||||
|
(defn delete-button
|
||||||
|
[recording? ready-to-delete? reviewing-audio?]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [opacity (reanimated/use-shared-value 0)
|
||||||
|
translate-x (reanimated/use-shared-value 20)
|
||||||
|
scale (reanimated/use-shared-value 1)
|
||||||
|
connector-opacity (reanimated/use-shared-value 0)
|
||||||
|
connector-width (reanimated/use-shared-value 24)
|
||||||
|
connector-height (reanimated/use-shared-value 12)
|
||||||
|
border-radius-first-half (reanimated/use-shared-value 8)
|
||||||
|
border-radius-second-half (reanimated/use-shared-value 8)
|
||||||
|
start-x-animation (fn []
|
||||||
|
(animate-linear-with-delay translate-x 12 50 133.33)
|
||||||
|
(animate-easing-with-delay connector-opacity 1 0 93.33)
|
||||||
|
(animate-easing-with-delay connector-width 56 83.33 80)
|
||||||
|
(animate-easing-with-delay connector-height 56 83.33 80)
|
||||||
|
(animate-easing-with-delay border-radius-first-half 28 83.33 80)
|
||||||
|
(animate-easing-with-delay border-radius-second-half 28 83.33 80))
|
||||||
|
reset-x-animation (fn []
|
||||||
|
(animate-linear translate-x 0 100)
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value connector-width 24)
|
||||||
|
(set-value connector-height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))
|
||||||
|
fade-in-animation (fn []
|
||||||
|
(animate-linear translate-x 0 200)
|
||||||
|
(animate-linear opacity 1 200))
|
||||||
|
fade-out-animation (fn []
|
||||||
|
(animate-linear
|
||||||
|
translate-x
|
||||||
|
(if @reviewing-audio? 35 20)
|
||||||
|
200)
|
||||||
|
(if @reviewing-audio?
|
||||||
|
(animate-linear scale 0.75 200)
|
||||||
|
(animate-linear opacity 0 200))
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value connector-width 24)
|
||||||
|
(set-value connector-height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))
|
||||||
|
fade-out-reset-animation (fn []
|
||||||
|
(animate-linear opacity 0 200)
|
||||||
|
(animate-linear-with-delay translate-x 20 0 200)
|
||||||
|
(animate-linear-with-delay scale 1 0 200))]
|
||||||
|
(use-effect (fn []
|
||||||
|
(if @recording?
|
||||||
|
(fade-in-animation)
|
||||||
|
(fade-out-animation)))
|
||||||
|
[@recording?])
|
||||||
|
(use-effect (fn []
|
||||||
|
(when-not @reviewing-audio?
|
||||||
|
(fade-out-reset-animation)))
|
||||||
|
[@reviewing-audio?])
|
||||||
|
(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-connector connector-opacity
|
||||||
|
connector-width
|
||||||
|
connector-height
|
||||||
|
border-radius-first-half
|
||||||
|
border-radius-second-half)}]]
|
||||||
|
[reanimated/view
|
||||||
|
{:style (style/delete-button scale translate-x opacity)
|
||||||
|
:pointer-events :none}
|
||||||
|
[icons/icon :i/delete
|
||||||
|
{:color colors/white
|
||||||
|
:size 20}]]]))])
|
|
@ -0,0 +1,85 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.buttons.lock-button
|
||||||
|
(:require [quo2.components.icon :as icons]
|
||||||
|
[quo2.components.record-audio.record-audio.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.reanimated :as reanimated]
|
||||||
|
[react-native.core :refer [use-effect]]
|
||||||
|
[quo2.components.record-audio.record-audio.helpers :refer
|
||||||
|
[animate-linear-with-delay
|
||||||
|
animate-easing-with-delay
|
||||||
|
animate-linear
|
||||||
|
set-value]]))
|
||||||
|
|
||||||
|
(defn lock-button
|
||||||
|
[recording? ready-to-lock? locked?]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [translate-x-y (reanimated/use-shared-value 20)
|
||||||
|
opacity (reanimated/use-shared-value 0)
|
||||||
|
connector-opacity (reanimated/use-shared-value 0)
|
||||||
|
width (reanimated/use-shared-value 24)
|
||||||
|
height (reanimated/use-shared-value 12)
|
||||||
|
border-radius-first-half (reanimated/use-shared-value 8)
|
||||||
|
border-radius-second-half (reanimated/use-shared-value 8)
|
||||||
|
start-x-y-animation (fn []
|
||||||
|
(animate-linear-with-delay translate-x-y 8 50 116.66)
|
||||||
|
(animate-easing-with-delay connector-opacity 1 0 80)
|
||||||
|
(animate-easing-with-delay width 56 83.33 63.33)
|
||||||
|
(animate-easing-with-delay height 56 83.33 63.33)
|
||||||
|
(animate-easing-with-delay border-radius-first-half
|
||||||
|
28
|
||||||
|
83.33
|
||||||
|
63.33)
|
||||||
|
(animate-easing-with-delay border-radius-second-half
|
||||||
|
28
|
||||||
|
83.33
|
||||||
|
63.33))
|
||||||
|
reset-x-y-animation (fn []
|
||||||
|
(animate-linear translate-x-y 0 100)
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value width 24)
|
||||||
|
(set-value height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))
|
||||||
|
fade-in-animation (fn []
|
||||||
|
(animate-linear translate-x-y 0 220)
|
||||||
|
(animate-linear opacity 1 220))
|
||||||
|
fade-out-animation (fn []
|
||||||
|
(animate-linear translate-x-y 20 200)
|
||||||
|
(animate-linear opacity 0 200)
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value width 24)
|
||||||
|
(set-value height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))]
|
||||||
|
(use-effect (fn []
|
||||||
|
(if @recording?
|
||||||
|
(fade-in-animation)
|
||||||
|
(fade-out-animation)))
|
||||||
|
[@recording?])
|
||||||
|
(use-effect (fn []
|
||||||
|
(cond
|
||||||
|
@ready-to-lock?
|
||||||
|
(start-x-y-animation)
|
||||||
|
(and @recording? (not @locked?))
|
||||||
|
(reset-x-y-animation)))
|
||||||
|
[@ready-to-lock?])
|
||||||
|
(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-connector connector-opacity
|
||||||
|
width
|
||||||
|
height
|
||||||
|
border-radius-first-half
|
||||||
|
border-radius-second-half)}]]
|
||||||
|
[reanimated/view
|
||||||
|
{:style (style/lock-button translate-x-y opacity)
|
||||||
|
:pointer-events :none}
|
||||||
|
[icons/icon (if @ready-to-lock? :i/locked :i/unlocked)
|
||||||
|
{:color (colors/theme-colors colors/black colors/white)
|
||||||
|
:size 20}]]]))])
|
|
@ -0,0 +1,28 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.buttons.record-button
|
||||||
|
(:require [quo2.components.icon :as icons]
|
||||||
|
[quo2.components.record-audio.record-audio.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.core :as rn :refer [use-effect]]
|
||||||
|
[react-native.reanimated :as reanimated]
|
||||||
|
[quo2.components.buttons.button :as button]
|
||||||
|
[quo2.components.record-audio.record-audio.helpers :refer [set-value]]))
|
||||||
|
|
||||||
|
(defn record-button
|
||||||
|
[recording? reviewing-audio?]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [opacity (reanimated/use-shared-value 1)
|
||||||
|
show-animation #(set-value opacity 1)
|
||||||
|
hide-animation #(set-value opacity 0)]
|
||||||
|
(use-effect (fn []
|
||||||
|
(if (or @recording? @reviewing-audio?)
|
||||||
|
(hide-animation)
|
||||||
|
(show-animation)))
|
||||||
|
[@recording? @reviewing-audio?])
|
||||||
|
[reanimated/view {:style (style/record-button-container opacity)}
|
||||||
|
[button/button
|
||||||
|
{:type :outline
|
||||||
|
:size 32
|
||||||
|
:width 32
|
||||||
|
:accessibility-label :mic-button}
|
||||||
|
[icons/icon :i/audio {:color (colors/theme-colors colors/neutral-100 colors/white)}]]]))])
|
|
@ -0,0 +1,212 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.buttons.record-button-big
|
||||||
|
(:require [quo.react :refer [memo]]
|
||||||
|
[quo2.components.icon :as icons]
|
||||||
|
[quo2.components.record-audio.record-audio.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.core :as rn :refer [use-effect]]
|
||||||
|
[react-native.reanimated :as reanimated]
|
||||||
|
[status-im.audio.core :as audio]
|
||||||
|
[taoensso.timbre :as log]
|
||||||
|
[cljs-bean.core :as bean]
|
||||||
|
[reagent.core :as reagent]
|
||||||
|
[quo2.components.record-audio.record-audio.helpers :refer
|
||||||
|
[animate-linear
|
||||||
|
animate-linear-with-delay
|
||||||
|
animate-linear-with-delay-loop
|
||||||
|
animate-easing
|
||||||
|
set-value]]))
|
||||||
|
|
||||||
|
(def ^:private scale-to-each 1.8)
|
||||||
|
(def ^:private scale-to-total 2.6)
|
||||||
|
(def ^:private scale-padding 0.16)
|
||||||
|
(def ^:private opacity-from-lock 1)
|
||||||
|
(def ^:private opacity-from-default 0.5)
|
||||||
|
(def ^:private signal-anim-duration 3900)
|
||||||
|
(def ^:private signal-anim-duration-2 1950)
|
||||||
|
|
||||||
|
(def ^:private record-audio-worklets (js/require "../src/js/record_audio_worklets.js"))
|
||||||
|
|
||||||
|
(defn- ring-scale
|
||||||
|
[scale substract]
|
||||||
|
(.ringScale ^js record-audio-worklets
|
||||||
|
scale
|
||||||
|
substract))
|
||||||
|
|
||||||
|
(def ^:private animated-ring
|
||||||
|
(reagent/adapt-react-class
|
||||||
|
(memo
|
||||||
|
(fn [props]
|
||||||
|
(let [{:keys [scale opacity color]} (bean/bean props)]
|
||||||
|
(reagent/as-element
|
||||||
|
[reanimated/view {:style (style/animated-circle scale opacity color)}]))))))
|
||||||
|
|
||||||
|
(defn record-button-big
|
||||||
|
[recording? ready-to-send? ready-to-lock? ready-to-delete? record-button-is-animating?
|
||||||
|
record-button-at-initial-position? locked? reviewing-audio? recording-timer recording-length-ms
|
||||||
|
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [scale (reanimated/use-shared-value 1)
|
||||||
|
opacity (reanimated/use-shared-value 0)
|
||||||
|
opacity-from (if @ready-to-lock? opacity-from-lock opacity-from-default)
|
||||||
|
animations (map
|
||||||
|
(fn [index]
|
||||||
|
(let [ring-scale (ring-scale scale (* scale-padding index))]
|
||||||
|
{:scale ring-scale
|
||||||
|
:opacity (reanimated/interpolate ring-scale
|
||||||
|
[1 scale-to-each]
|
||||||
|
[opacity-from 0])}))
|
||||||
|
(range 0 5))
|
||||||
|
rings-color (cond
|
||||||
|
@ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque
|
||||||
|
colors/neutral-80)
|
||||||
|
@ready-to-delete? colors/danger-50
|
||||||
|
:else colors/primary-50)
|
||||||
|
translate-y (reanimated/use-shared-value 0)
|
||||||
|
translate-x (reanimated/use-shared-value 0)
|
||||||
|
button-color colors/primary-50
|
||||||
|
icon-color (if (and (not (colors/dark?)) @ready-to-lock?) colors/black colors/white)
|
||||||
|
icon-opacity (reanimated/use-shared-value 1)
|
||||||
|
red-overlay-opacity (reanimated/use-shared-value 0)
|
||||||
|
gray-overlay-opacity (reanimated/use-shared-value 0)
|
||||||
|
complete-animation (fn []
|
||||||
|
(cond
|
||||||
|
(and @ready-to-lock? (not @record-button-is-animating?))
|
||||||
|
(do
|
||||||
|
(reset! locked? true)
|
||||||
|
(reset! ready-to-lock? false))
|
||||||
|
(and (not @locked?) (not @reviewing-audio?))
|
||||||
|
(audio/stop-recording
|
||||||
|
@recorder-ref
|
||||||
|
(fn []
|
||||||
|
(cond
|
||||||
|
@ready-to-send?
|
||||||
|
(when on-send
|
||||||
|
(on-send (audio/get-recorder-file-path @recorder-ref)))
|
||||||
|
@ready-to-delete?
|
||||||
|
(when on-cancel
|
||||||
|
(on-cancel)))
|
||||||
|
(reload-recorder-fn)
|
||||||
|
(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)
|
||||||
|
(js/clearInterval @recording-timer)
|
||||||
|
(reset! recording-length-ms 0)
|
||||||
|
(log/debug "[record-audio] stop recording - success"))
|
||||||
|
#(log/error "[record-audio] stop recording - error: " %))))
|
||||||
|
start-animation (fn []
|
||||||
|
(set-value opacity 1)
|
||||||
|
(animate-linear scale 2.6 signal-anim-duration)
|
||||||
|
;; TODO: Research if we can implement this with withSequence method
|
||||||
|
;; from Reanimated 2
|
||||||
|
;; GitHub issue [#14561]:
|
||||||
|
;; https://github.com/status-im/status-mobile/issues/14561
|
||||||
|
(reset! clear-timeout
|
||||||
|
(js/setTimeout
|
||||||
|
(fn []
|
||||||
|
(set-value scale scale-to-each)
|
||||||
|
(animate-linear-with-delay-loop scale
|
||||||
|
scale-to-total
|
||||||
|
signal-anim-duration-2
|
||||||
|
0))
|
||||||
|
signal-anim-duration)))
|
||||||
|
stop-animation (fn []
|
||||||
|
(set-value opacity 0)
|
||||||
|
(reanimated/cancel-animation scale)
|
||||||
|
(set-value scale 1)
|
||||||
|
(when @clear-timeout (js/clearTimeout @clear-timeout)))
|
||||||
|
start-y-animation (fn []
|
||||||
|
(reset! record-button-at-initial-position? false)
|
||||||
|
(reset! record-button-is-animating? true)
|
||||||
|
(animate-easing translate-y -64 250)
|
||||||
|
(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))
|
||||||
|
reset-y-animation (fn []
|
||||||
|
(animate-easing translate-y 0 300)
|
||||||
|
(animate-linear icon-opacity 1 500)
|
||||||
|
(js/setTimeout (fn []
|
||||||
|
(reset! record-button-at-initial-position? true))
|
||||||
|
500))
|
||||||
|
start-x-animation (fn []
|
||||||
|
(reset! record-button-at-initial-position? false)
|
||||||
|
(reset! record-button-is-animating? true)
|
||||||
|
(animate-easing translate-x -64 250)
|
||||||
|
(animate-linear-with-delay icon-opacity 0 33.33 76.66)
|
||||||
|
(animate-linear red-overlay-opacity 1 33.33)
|
||||||
|
(js/setTimeout (fn []
|
||||||
|
(reset! record-button-is-animating? false)
|
||||||
|
(when-not @touch-active? (complete-animation)))
|
||||||
|
250))
|
||||||
|
reset-x-animation (fn []
|
||||||
|
(animate-easing translate-x 0 300)
|
||||||
|
(animate-linear icon-opacity 1 500)
|
||||||
|
(animate-linear red-overlay-opacity 0 100)
|
||||||
|
(js/setTimeout (fn []
|
||||||
|
(reset! record-button-at-initial-position? true))
|
||||||
|
500))
|
||||||
|
start-x-y-animation (fn []
|
||||||
|
(reset! record-button-at-initial-position? false)
|
||||||
|
(reset! record-button-is-animating? true)
|
||||||
|
(animate-easing translate-y -44 200)
|
||||||
|
(animate-easing translate-x -44 200)
|
||||||
|
(animate-linear-with-delay icon-opacity 0 33.33 33.33)
|
||||||
|
(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 []
|
||||||
|
(animate-easing translate-y 0 300)
|
||||||
|
(animate-easing translate-x 0 300)
|
||||||
|
(animate-linear icon-opacity 1 500)
|
||||||
|
(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
|
||||||
|
{:style (style/record-button-big-container translate-x translate-y opacity)
|
||||||
|
:pointer-events :none}
|
||||||
|
[:<>
|
||||||
|
(map-indexed
|
||||||
|
(fn [id animation]
|
||||||
|
^{:key id}
|
||||||
|
[animated-ring
|
||||||
|
{:scale (:scale animation)
|
||||||
|
:opacity (:opacity animation)
|
||||||
|
:color rings-color}])
|
||||||
|
animations)]
|
||||||
|
[rn/view {:style (style/record-button-big-body button-color)}
|
||||||
|
[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-icon-container icon-opacity)}
|
||||||
|
(if @locked?
|
||||||
|
[rn/view {:style style/stop-icon}]
|
||||||
|
[icons/icon :i/audio {:color icon-color}])]]]))])
|
|
@ -0,0 +1,90 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.buttons.send-button
|
||||||
|
(:require [quo2.components.icon :as icons]
|
||||||
|
[quo2.components.record-audio.record-audio.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.reanimated :as reanimated]
|
||||||
|
[react-native.core :refer [use-effect]]
|
||||||
|
[quo2.components.record-audio.record-audio.helpers :refer
|
||||||
|
[animate-linear
|
||||||
|
animate-linear-with-delay
|
||||||
|
animate-easing-with-delay
|
||||||
|
set-value]]))
|
||||||
|
|
||||||
|
(defn send-button
|
||||||
|
[recording? ready-to-send? reviewing-audio?]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [opacity (reanimated/use-shared-value 0)
|
||||||
|
translate-y (reanimated/use-shared-value 20)
|
||||||
|
connector-opacity (reanimated/use-shared-value 0)
|
||||||
|
width (reanimated/use-shared-value 12)
|
||||||
|
height (reanimated/use-shared-value 24)
|
||||||
|
border-radius-first-half (reanimated/use-shared-value 16)
|
||||||
|
border-radius-second-half (reanimated/use-shared-value 8)
|
||||||
|
start-y-animation (fn []
|
||||||
|
(animate-linear-with-delay translate-y 12 50 133.33)
|
||||||
|
(animate-easing-with-delay connector-opacity 1 0 93.33)
|
||||||
|
(animate-easing-with-delay width 56 83.33 80)
|
||||||
|
(animate-easing-with-delay height 56 83.33 80)
|
||||||
|
(animate-easing-with-delay border-radius-first-half 28 83.33 80)
|
||||||
|
(animate-easing-with-delay border-radius-second-half 28 83.33 80))
|
||||||
|
reset-y-animation (fn []
|
||||||
|
(animate-linear translate-y 0 100)
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value width 12)
|
||||||
|
(set-value height 24)
|
||||||
|
(set-value border-radius-first-half 16)
|
||||||
|
(set-value border-radius-second-half 8))
|
||||||
|
fade-in-animation (fn []
|
||||||
|
(animate-linear translate-y 0 200)
|
||||||
|
(animate-linear opacity 1 200))
|
||||||
|
fade-out-animation (fn []
|
||||||
|
(animate-linear
|
||||||
|
translate-y
|
||||||
|
(if @reviewing-audio? 76 20)
|
||||||
|
200)
|
||||||
|
(when-not @reviewing-audio?
|
||||||
|
(animate-linear opacity 0 200))
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value width 24)
|
||||||
|
(set-value height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))
|
||||||
|
fade-out-reset-animation (fn []
|
||||||
|
(animate-linear opacity 0 200)
|
||||||
|
(animate-linear-with-delay translate-y 20 0 200)
|
||||||
|
(set-value connector-opacity 0)
|
||||||
|
(set-value width 24)
|
||||||
|
(set-value height 12)
|
||||||
|
(set-value border-radius-first-half 8)
|
||||||
|
(set-value border-radius-second-half 16))]
|
||||||
|
(use-effect (fn []
|
||||||
|
(if @recording?
|
||||||
|
(fade-in-animation)
|
||||||
|
(fade-out-animation)))
|
||||||
|
[@recording?])
|
||||||
|
(use-effect (fn []
|
||||||
|
(when-not @reviewing-audio?
|
||||||
|
(fade-out-reset-animation)))
|
||||||
|
[@reviewing-audio?])
|
||||||
|
(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-connector connector-opacity
|
||||||
|
width
|
||||||
|
height
|
||||||
|
border-radius-first-half
|
||||||
|
border-radius-second-half)}]]
|
||||||
|
[reanimated/view
|
||||||
|
{:style (style/send-button translate-y opacity)
|
||||||
|
:pointer-events :none}
|
||||||
|
[icons/icon :i/arrow-up
|
||||||
|
{:color colors/white
|
||||||
|
:size 20
|
||||||
|
:container-style style/send-icon-container}]]]))])
|
|
@ -0,0 +1,50 @@
|
||||||
|
(ns quo2.components.record-audio.record-audio.helpers
|
||||||
|
(:require [react-native.reanimated :as reanimated]))
|
||||||
|
|
||||||
|
(defn animate-linear
|
||||||
|
[shared-value value duration]
|
||||||
|
(reanimated/animate-shared-value-with-timing
|
||||||
|
shared-value
|
||||||
|
value
|
||||||
|
duration
|
||||||
|
:linear))
|
||||||
|
|
||||||
|
(defn animate-linear-with-delay
|
||||||
|
[shared-value value duration delay]
|
||||||
|
(reanimated/animate-shared-value-with-delay
|
||||||
|
shared-value
|
||||||
|
value
|
||||||
|
duration
|
||||||
|
:linear
|
||||||
|
delay))
|
||||||
|
|
||||||
|
(defn animate-linear-with-delay-loop
|
||||||
|
[shared-value value duration delay]
|
||||||
|
(reanimated/animate-shared-value-with-delay-repeat
|
||||||
|
shared-value
|
||||||
|
value
|
||||||
|
duration
|
||||||
|
:linear
|
||||||
|
delay
|
||||||
|
-1))
|
||||||
|
|
||||||
|
(defn animate-easing
|
||||||
|
[shared-value value duration]
|
||||||
|
(reanimated/animate-shared-value-with-timing
|
||||||
|
shared-value
|
||||||
|
value
|
||||||
|
duration
|
||||||
|
:easing1))
|
||||||
|
|
||||||
|
(defn animate-easing-with-delay
|
||||||
|
[shared-value value duration delay]
|
||||||
|
(reanimated/animate-shared-value-with-delay
|
||||||
|
shared-value
|
||||||
|
value
|
||||||
|
duration
|
||||||
|
:easing1
|
||||||
|
delay))
|
||||||
|
|
||||||
|
(defn set-value
|
||||||
|
[shared-value value]
|
||||||
|
(reanimated/set-shared-value shared-value value))
|
|
@ -199,9 +199,10 @@
|
||||||
:z-index 0}))
|
:z-index 0}))
|
||||||
|
|
||||||
(defn delete-button
|
(defn delete-button
|
||||||
[translate-x opacity]
|
[scale translate-x opacity]
|
||||||
(reanimated/apply-animations-to-style
|
(reanimated/apply-animations-to-style
|
||||||
{:transform [{:translateX translate-x}]
|
{:transform [{:translateX translate-x}
|
||||||
|
{:scale scale}]
|
||||||
:opacity opacity}
|
:opacity opacity}
|
||||||
{:width 32
|
{:width 32
|
||||||
:height 32
|
:height 32
|
||||||
|
@ -221,8 +222,66 @@
|
||||||
{:margin-bottom 32
|
{:margin-bottom 32
|
||||||
:margin-right 32}))
|
:margin-right 32}))
|
||||||
|
|
||||||
(def input-container
|
(def button-container
|
||||||
{:width 140
|
{:width 140
|
||||||
:height 140
|
:height 140
|
||||||
:align-items :flex-end
|
:align-items :flex-end
|
||||||
:justify-content :flex-end})
|
:justify-content :flex-end
|
||||||
|
:position :absolute
|
||||||
|
:right -10})
|
||||||
|
|
||||||
|
(def bar-container
|
||||||
|
{:flex 1
|
||||||
|
:height 128})
|
||||||
|
|
||||||
|
(defn recording-bar-container
|
||||||
|
[]
|
||||||
|
{:height 4
|
||||||
|
:border-radius 2
|
||||||
|
:background-color (colors/theme-colors colors/neutral-20 colors/neutral-80)
|
||||||
|
:overflow :hidden
|
||||||
|
:position :absolute
|
||||||
|
:left 80
|
||||||
|
:right 148
|
||||||
|
:bottom 34})
|
||||||
|
|
||||||
|
(defn recording-bar
|
||||||
|
[fill-percentage ready-to-delete?]
|
||||||
|
{:width (str fill-percentage "%")
|
||||||
|
:height 4
|
||||||
|
:border-radius 2
|
||||||
|
:background-color (if ready-to-delete?
|
||||||
|
(colors/theme-colors colors/danger-50 colors/danger-60)
|
||||||
|
(colors/theme-colors colors/primary-50 colors/primary-60))})
|
||||||
|
|
||||||
|
(defn timer-container
|
||||||
|
[reviewing-audio?]
|
||||||
|
{:position :absolute
|
||||||
|
:left (if reviewing-audio? 67 20)
|
||||||
|
:bottom 28.5
|
||||||
|
:flex-direction :row
|
||||||
|
:align-items :center})
|
||||||
|
|
||||||
|
(defn timer-circle
|
||||||
|
[]
|
||||||
|
{:width 8
|
||||||
|
:height 8
|
||||||
|
:border-radius 4
|
||||||
|
:margin-right 6
|
||||||
|
:background-color (colors/theme-colors colors/danger-50 colors/danger-60)})
|
||||||
|
|
||||||
|
(defn timer-text
|
||||||
|
[]
|
||||||
|
{:color (colors/theme-colors colors/danger-50 colors/danger-60)})
|
||||||
|
|
||||||
|
(defn play-button
|
||||||
|
[]
|
||||||
|
{:position :absolute
|
||||||
|
:bottom 20
|
||||||
|
:left 20
|
||||||
|
:width 32
|
||||||
|
:height 32
|
||||||
|
:border-radius 16
|
||||||
|
:align-items :center
|
||||||
|
:justify-content :center
|
||||||
|
:background-color (colors/theme-colors colors/neutral-10 colors/neutral-90)})
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
(ns quo2.components.record-audio.soundtrack.--tests--.soundtrack-component-spec
|
||||||
|
(:require [quo2.components.record-audio.soundtrack.view :as soundtrack]
|
||||||
|
[test-helpers.component :as h]
|
||||||
|
[reagent.core :as reagent]
|
||||||
|
[status-im.audio.core :as audio]))
|
||||||
|
|
||||||
|
(h/describe "soundtrack component"
|
||||||
|
(h/before-each
|
||||||
|
(fn []
|
||||||
|
(h/use-fake-timers)))
|
||||||
|
|
||||||
|
(h/after-each
|
||||||
|
(fn []
|
||||||
|
(h/clear-all-timers)
|
||||||
|
(h/use-real-timers)))
|
||||||
|
|
||||||
|
(h/test "renders soundtrack"
|
||||||
|
(with-redefs [audio/get-player-duration (fn [] 2000)]
|
||||||
|
(let [player-ref (reagent/atom {})
|
||||||
|
audio-current-time-ms (reagent/atom 0)]
|
||||||
|
(h/render [soundtrack/soundtrack
|
||||||
|
{:player-ref player-ref
|
||||||
|
:audio-current-time-ms audio-current-time-ms}])
|
||||||
|
(-> (h/expect (h/get-by-test-id "soundtrack"))
|
||||||
|
(.toBeTruthy)))))
|
||||||
|
|
||||||
|
(h/test "soundtrack on-sliding-start works"
|
||||||
|
(with-redefs [audio/get-player-duration (fn [] 2000)]
|
||||||
|
(let [seeking-audio? (reagent/atom false)
|
||||||
|
player-ref (reagent/atom {})
|
||||||
|
audio-current-time-ms (reagent/atom 0)]
|
||||||
|
(h/render [soundtrack/soundtrack
|
||||||
|
{:seeking-audio? seeking-audio?
|
||||||
|
:player-ref player-ref
|
||||||
|
:audio-current-time-ms audio-current-time-ms}])
|
||||||
|
(h/fire-event
|
||||||
|
:on-sliding-start
|
||||||
|
(h/get-by-test-id "soundtrack"))
|
||||||
|
(-> (h/expect @seeking-audio?)
|
||||||
|
(.toBe true)))))
|
||||||
|
|
||||||
|
(h/test "soundtrack on-sliding-complete works"
|
||||||
|
(with-redefs [audio/get-player-duration (fn [] 2000)
|
||||||
|
audio/seek-player (js/jest.fn)]
|
||||||
|
(let [seeking-audio? (reagent/atom false)
|
||||||
|
player-ref (reagent/atom {})
|
||||||
|
audio-current-time-ms (reagent/atom 0)]
|
||||||
|
(h/render [soundtrack/soundtrack
|
||||||
|
{:seeking-audio? seeking-audio?
|
||||||
|
:player-ref player-ref
|
||||||
|
:audio-current-time-ms audio-current-time-ms}])
|
||||||
|
(h/fire-event
|
||||||
|
:on-sliding-start
|
||||||
|
(h/get-by-test-id "soundtrack"))
|
||||||
|
(h/fire-event
|
||||||
|
:on-sliding-complete
|
||||||
|
(h/get-by-test-id "soundtrack")
|
||||||
|
1000)
|
||||||
|
(-> (h/expect @seeking-audio?)
|
||||||
|
(.toBe false))
|
||||||
|
(-> (h/expect audio/seek-player)
|
||||||
|
(.toHaveBeenCalledTimes 1)))))
|
||||||
|
|
||||||
|
(h/test "soundtrack on-value-change when seeking audio works"
|
||||||
|
(with-redefs [audio/get-player-duration (fn [] 2000)
|
||||||
|
audio/seek-player (js/jest.fn)]
|
||||||
|
(let [seeking-audio? (reagent/atom false)
|
||||||
|
player-ref (reagent/atom {})
|
||||||
|
audio-current-time-ms (reagent/atom 0)]
|
||||||
|
(h/render [soundtrack/soundtrack
|
||||||
|
{:seeking-audio? seeking-audio?
|
||||||
|
:player-ref player-ref
|
||||||
|
:audio-current-time-ms audio-current-time-ms}])
|
||||||
|
(h/fire-event
|
||||||
|
:on-sliding-start
|
||||||
|
(h/get-by-test-id "soundtrack"))
|
||||||
|
(h/fire-event
|
||||||
|
:on-value-change
|
||||||
|
(h/get-by-test-id "soundtrack")
|
||||||
|
1000)
|
||||||
|
(-> (h/expect @audio-current-time-ms)
|
||||||
|
(.toBe 1000))))))
|
|
@ -0,0 +1,16 @@
|
||||||
|
(ns quo2.components.record-audio.soundtrack.style
|
||||||
|
(:require [react-native.platform :as platform]))
|
||||||
|
|
||||||
|
(defn player-slider-container
|
||||||
|
[]
|
||||||
|
(merge
|
||||||
|
{:position :absolute
|
||||||
|
:left (if platform/ios? 115 104)
|
||||||
|
:right (if platform/ios? 108 92)
|
||||||
|
:bottom (if platform/ios? 16 27)}
|
||||||
|
(when platform/android?
|
||||||
|
;; Workaround to increase the thickness of the slider track on Android
|
||||||
|
;; which is currently not supported by the Slider library and remove
|
||||||
|
;; the thumb shadow that appears when dragging.
|
||||||
|
{:transform [{:scaleY 2}]
|
||||||
|
:background-color :transparent})))
|
|
@ -0,0 +1,38 @@
|
||||||
|
(ns quo2.components.record-audio.soundtrack.view
|
||||||
|
(:require [quo2.components.record-audio.soundtrack.style :as style]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[status-im.audio.core :as audio]
|
||||||
|
[taoensso.timbre :as log]
|
||||||
|
[react-native.platform :as platform]
|
||||||
|
[react-native.slider :as slider]))
|
||||||
|
|
||||||
|
(def ^:private thumb-light (js/require "../resources/images/icons2/12x12/thumb-light.png"))
|
||||||
|
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png"))
|
||||||
|
|
||||||
|
(defn soundtrack
|
||||||
|
[{:keys [audio-current-time-ms player-ref seeking-audio?]}]
|
||||||
|
[:f>
|
||||||
|
(fn []
|
||||||
|
(let [audio-duration-ms (audio/get-player-duration @player-ref)]
|
||||||
|
[:<>
|
||||||
|
[slider/slider
|
||||||
|
{:test-ID "soundtrack"
|
||||||
|
:style (style/player-slider-container)
|
||||||
|
:minimum-value 0
|
||||||
|
:maximum-value audio-duration-ms
|
||||||
|
:value @audio-current-time-ms
|
||||||
|
:on-sliding-start #(reset! seeking-audio? true)
|
||||||
|
:on-sliding-complete (fn [seek-time]
|
||||||
|
(reset! seeking-audio? false)
|
||||||
|
(audio/seek-player
|
||||||
|
@player-ref
|
||||||
|
seek-time
|
||||||
|
#(log/debug "[record-audio] on seek - seek time: " seek-time)
|
||||||
|
#(log/error "[record-audio] on seek - error: " %)))
|
||||||
|
:on-value-change #(when @seeking-audio?
|
||||||
|
(reset! audio-current-time-ms %))
|
||||||
|
:thumb-image (if (colors/dark?) thumb-dark thumb-light)
|
||||||
|
:minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60)
|
||||||
|
: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))}]]))])
|
|
@ -7,4 +7,6 @@
|
||||||
[quo2.components.drawers.permission-context.component-spec]
|
[quo2.components.drawers.permission-context.component-spec]
|
||||||
[quo2.components.markdown.--tests--.text-component-spec]
|
[quo2.components.markdown.--tests--.text-component-spec]
|
||||||
[quo2.components.selectors.--tests--.selectors-component-spec]
|
[quo2.components.selectors.--tests--.selectors-component-spec]
|
||||||
[quo2.components.selectors.filter.component-spec]))
|
[quo2.components.selectors.filter.component-spec]
|
||||||
|
[quo2.components.record-audio.record-audio.--tests--.record-audio-component-spec]
|
||||||
|
[quo2.components.record-audio.soundtrack.--tests--.soundtrack-component-spec]))
|
||||||
|
|
|
@ -94,11 +94,12 @@
|
||||||
|
|
||||||
(defn use-effect
|
(defn use-effect
|
||||||
([effect-fn]
|
([effect-fn]
|
||||||
|
(use-effect effect-fn []))
|
||||||
|
([effect-fn deps]
|
||||||
(react/useEffect
|
(react/useEffect
|
||||||
#(let [ret (effect-fn)]
|
#(let [ret (effect-fn)]
|
||||||
(if (fn? ret) ret js/undefined))))
|
(if (fn? ret) ret js/undefined))
|
||||||
([effect-fn deps]
|
(bean/->js deps))))
|
||||||
(react/useEffect effect-fn (bean/->js deps))))
|
|
||||||
|
|
||||||
(defn use-effect-once
|
(defn use-effect-once
|
||||||
[effect-fn]
|
[effect-fn]
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
(ns react-native.permissions
|
||||||
|
(:require ["react-native-permissions" :refer (check requestMultiple PERMISSIONS RESULTS)]
|
||||||
|
[react-native.platform :as platform]))
|
||||||
|
|
||||||
|
(def permissions-map
|
||||||
|
{:read-external-storage (cond
|
||||||
|
platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
|
||||||
|
:write-external-storage (cond
|
||||||
|
platform/low-device? (.-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 (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals))))))
|
||||||
|
|
||||||
|
(defn request-permissions
|
||||||
|
[{:keys [permissions on-allowed on-denied]
|
||||||
|
:or {on-allowed #()
|
||||||
|
on-denied #()}}]
|
||||||
|
(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)))))
|
||||||
|
|
||||||
|
(defn permission-granted?
|
||||||
|
[permission on-result on-error]
|
||||||
|
(-> (check (get permissions-map permission))
|
||||||
|
(.then #(on-result (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} %))))
|
||||||
|
(.catch #(on-error %))))
|
|
@ -0,0 +1,5 @@
|
||||||
|
(ns react-native.slider
|
||||||
|
(:require ["@react-native-community/slider" :default Slider]
|
||||||
|
[reagent.core :as reagent]))
|
||||||
|
|
||||||
|
(def slider (reagent/adapt-react-class Slider))
|
|
@ -1,16 +1,29 @@
|
||||||
(ns status-im2.contexts.quo-preview.record-audio.record-audio
|
(ns status-im2.contexts.quo-preview.record-audio.record-audio
|
||||||
(:require [quo2.components.record-audio.record-audio.view :as record-audio]
|
(:require [quo2.components.record-audio.record-audio.view :as record-audio]
|
||||||
[react-native.core :as rn]))
|
[quo2.core :as quo]
|
||||||
|
[react-native.core :as rn]
|
||||||
|
[reagent.core :as reagent]))
|
||||||
|
|
||||||
(defn cool-preview
|
(defn cool-preview
|
||||||
[]
|
[]
|
||||||
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
|
(let [message (reagent/atom "Press & hold the mic button to start recording...")
|
||||||
|
on-send #(reset! message (str "onSend event triggered. File path: " %))
|
||||||
|
on-start-recording #(reset! message "onStartRecording event triggered.")
|
||||||
|
on-reviewing-audio #(reset! message "onReviewingAudio event triggered.")
|
||||||
|
on-cancel #(reset! message "onCancel event triggered.")]
|
||||||
|
(fn []
|
||||||
[rn/view
|
[rn/view
|
||||||
[rn/view
|
[rn/view
|
||||||
{:padding-top 150
|
{:padding-top 150
|
||||||
:align-items :center
|
:align-items :center
|
||||||
:background-color :transparent}
|
:background-color :transparent
|
||||||
[record-audio/input-view]]]])
|
:flex-direction :row}
|
||||||
|
[record-audio/record-audio
|
||||||
|
{:on-send on-send
|
||||||
|
:on-start-recording on-start-recording
|
||||||
|
:on-reviewing-audio on-reviewing-audio
|
||||||
|
:on-cancel on-cancel}]]
|
||||||
|
[quo/text {:style {:margin-horizontal 20}} @message]])))
|
||||||
|
|
||||||
(defn preview-record-audio
|
(defn preview-record-audio
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -15,3 +15,15 @@
|
||||||
`(js/global.test
|
`(js/global.test
|
||||||
~description
|
~description
|
||||||
(fn [] ~@body)))
|
(fn [] ~@body)))
|
||||||
|
|
||||||
|
(defmacro before-each
|
||||||
|
[description & body]
|
||||||
|
`(js/beforeEach
|
||||||
|
~description
|
||||||
|
(fn [] ~@body)))
|
||||||
|
|
||||||
|
(defmacro after-each
|
||||||
|
[description & body]
|
||||||
|
`(js/afterEach
|
||||||
|
~description
|
||||||
|
(fn [] ~@body)))
|
||||||
|
|
|
@ -10,8 +10,13 @@
|
||||||
(rtl/render (reagent/as-element component)))
|
(rtl/render (reagent/as-element component)))
|
||||||
|
|
||||||
(defn fire-event
|
(defn fire-event
|
||||||
[event-name element]
|
([event-name element]
|
||||||
(rtl/fireEvent element (camel-snake-kebab/->camelCaseString event-name)))
|
(fire-event event-name element nil))
|
||||||
|
([event-name element data]
|
||||||
|
(rtl/fireEvent
|
||||||
|
element
|
||||||
|
(camel-snake-kebab/->camelCaseString event-name)
|
||||||
|
(clj->js data))))
|
||||||
|
|
||||||
(defn debug
|
(defn debug
|
||||||
[element]
|
[element]
|
||||||
|
@ -30,3 +35,13 @@
|
||||||
(rtl/screen.getByLabelText (name label)))
|
(rtl/screen.getByLabelText (name label)))
|
||||||
|
|
||||||
(defn expect [match] (js/expect match))
|
(defn expect [match] (js/expect match))
|
||||||
|
|
||||||
|
(defn use-fake-timers [] (js/jest.useFakeTimers))
|
||||||
|
|
||||||
|
(defn clear-all-timers [] (js/jest.clearAllTimers))
|
||||||
|
|
||||||
|
(defn use-real-timers [] (js/jest.useRealTimers))
|
||||||
|
|
||||||
|
(defn advance-timers-by-time
|
||||||
|
[time-ms]
|
||||||
|
(js/jest.advanceTimersByTime time-ms))
|
||||||
|
|
|
@ -12,7 +12,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
"testTimeout": 60000,
|
"testTimeout": 60000,
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!(@react-native|react-native-haptic-feedback|react-native-redash|react-native-image-crop-picker|@react-native-community|react-native-linear-gradient|react-native-background-timer|react-native|rn-emoji-keyboard|react-native-languages|react-native-shake|react-native-reanimated)/).*/"
|
"/node_modules/(?!(@react-native|react-native-haptic-feedback|react-native-redash|react-native-image-crop-picker|@react-native-community|react-native-linear-gradient|react-native-background-timer|react-native|rn-emoji-keyboard|react-native-languages|react-native-shake|react-native-reanimated|react-native-redash|react-native-permissions)/).*/"
|
||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
"__TEST__": true
|
"__TEST__": true
|
||||||
|
|
|
@ -3,16 +3,7 @@ const { NativeModules } = require('react-native');
|
||||||
|
|
||||||
require('@react-native-async-storage/async-storage/jest/async-storage-mock');
|
require('@react-native-async-storage/async-storage/jest/async-storage-mock');
|
||||||
require('react-native-gesture-handler/jestSetup');
|
require('react-native-gesture-handler/jestSetup');
|
||||||
|
require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests();
|
||||||
|
|
||||||
jest.mock('react-native-reanimated', () => {
|
|
||||||
const Reanimated = require('react-native-reanimated/mock');
|
|
||||||
// The mock for `call` immediately calls the callback which is incorrect
|
|
||||||
// So we override it with a no-op
|
|
||||||
Reanimated.default.call = () => { };
|
|
||||||
|
|
||||||
return Reanimated;
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
|
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
|
||||||
|
|
||||||
|
@ -35,6 +26,41 @@ jest.mock('react-native-languages', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-native-permissions', () =>
|
||||||
|
require('react-native-permissions/mock'),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('@react-native-community/audio-toolkit', () => ({
|
||||||
|
Recorder: jest.fn().mockImplementation(() => ({
|
||||||
|
prepare: jest.fn(),
|
||||||
|
record: jest.fn(),
|
||||||
|
toggleRecord: jest.fn(),
|
||||||
|
pause: jest.fn(),
|
||||||
|
stop: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
})),
|
||||||
|
Player: jest.fn().mockImplementation(() => ({
|
||||||
|
prepare: jest.fn(),
|
||||||
|
playPause: jest.fn(),
|
||||||
|
play: jest.fn(),
|
||||||
|
pause: jest.fn(),
|
||||||
|
stop: jest.fn(),
|
||||||
|
seek: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
})),
|
||||||
|
MediaStates: {
|
||||||
|
DESTROYED: -2,
|
||||||
|
ERROR: -1,
|
||||||
|
IDLE: 0,
|
||||||
|
PREPARING: 1,
|
||||||
|
PREPARED: 2,
|
||||||
|
SEEKING: 3,
|
||||||
|
PLAYING: 4,
|
||||||
|
RECORDING: 4,
|
||||||
|
PAUSED: 5
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
NativeModules.ReactLocalization = {
|
NativeModules.ReactLocalization = {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
|
|