Skip to content

Commit aeaa855

Browse files
feat: audio component
1 parent faa29a2 commit aeaa855

File tree

18 files changed

+342
-19
lines changed

18 files changed

+342
-19
lines changed
271 Bytes
Loading
367 Bytes
Loading
363 Bytes
Loading
524 Bytes
Loading

src/quo2/components/record_audio/record_audio/view.cljs

+1-1
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@
527527
[play-button playing-audio? player-ref playing-timer audio-current-time-ms seeking-audio?]
528528
[soundtrack/soundtrack
529529
{:audio-current-time-ms audio-current-time-ms
530-
:player-ref player-ref
530+
:player-ref @player-ref
531531
:seeking-audio? seeking-audio?}]])
532532
(when (or @recording? @reviewing-audio?)
533533
[time-counter @recording? @recording-length-ms @ready-to-delete? @reviewing-audio?

src/quo2/components/record_audio/soundtrack/__tests__/soundtrack_component_spec.cljs

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
(let [player-ref (reagent/atom {})
2020
audio-current-time-ms (reagent/atom 0)]
2121
(h/render [soundtrack/soundtrack
22-
{:player-ref player-ref
22+
{:player-ref @player-ref
2323
:audio-current-time-ms audio-current-time-ms}])
2424
(-> (h/expect (h/get-by-test-id "soundtrack"))
2525
(.toBeTruthy)))))
@@ -31,7 +31,7 @@
3131
audio-current-time-ms (reagent/atom 0)]
3232
(h/render [soundtrack/soundtrack
3333
{:seeking-audio? seeking-audio?
34-
:player-ref player-ref
34+
:player-ref @player-ref
3535
:audio-current-time-ms audio-current-time-ms}])
3636
(h/fire-event
3737
:on-sliding-start
@@ -47,7 +47,7 @@
4747
audio-current-time-ms (reagent/atom 0)]
4848
(h/render [soundtrack/soundtrack
4949
{:seeking-audio? seeking-audio?
50-
:player-ref player-ref
50+
:player-ref @player-ref
5151
:audio-current-time-ms audio-current-time-ms}])
5252
(h/fire-event
5353
:on-sliding-start
@@ -69,7 +69,7 @@
6969
audio-current-time-ms (reagent/atom 0)]
7070
(h/render [soundtrack/soundtrack
7171
{:seeking-audio? seeking-audio?
72-
:player-ref player-ref
72+
:player-ref @player-ref
7373
:audio-current-time-ms audio-current-time-ms}])
7474
(h/fire-event
7575
:on-sliding-start

src/quo2/components/record_audio/soundtrack/view.cljs

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,24 @@
1010
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png"))
1111

1212
(defn soundtrack
13-
[{:keys [audio-current-time-ms player-ref seeking-audio?]}]
13+
[{:keys [audio-current-time-ms player-ref style seeking-audio?]}]
1414
[:f>
1515
(fn []
16-
(let [audio-duration-ms (audio/get-player-duration @player-ref)]
16+
(let [audio-duration-ms (audio/get-player-duration player-ref)]
1717
[:<>
1818
[slider/slider
1919
{:test-ID "soundtrack"
20-
:style (style/player-slider-container)
20+
:style (merge
21+
(style/player-slider-container)
22+
(or style {}))
2123
:minimum-value 0
2224
:maximum-value audio-duration-ms
2325
:value @audio-current-time-ms
2426
:on-sliding-start #(reset! seeking-audio? true)
2527
:on-sliding-complete (fn [seek-time]
2628
(reset! seeking-audio? false)
2729
(audio/seek-player
28-
@player-ref
30+
player-ref
2931
seek-time
3032
#(log/debug "[record-audio] on seek - seek time: " seek-time)
3133
#(log/error "[record-audio] on seek - error: " %)))

src/quo2/core.cljs

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
quo2.components.profile.select-profile.view
6262
quo2.components.reactions.reaction
6363
quo2.components.record-audio.record-audio.view
64+
quo2.components.record-audio.soundtrack.view
6465
quo2.components.selectors.disclaimer.view
6566
quo2.components.selectors.filter.view
6667
quo2.components.selectors.selectors.view
@@ -183,6 +184,7 @@
183184

184185
;;;; RECORD AUDIO
185186
(def record-audio quo2.components.record-audio.record-audio.view/record-audio)
187+
(def soundtrack quo2.components.record-audio.soundtrack.view/soundtrack)
186188

187189
;;;; SETTINGS
188190
(def privacy-option quo2.components.settings.privacy-option/card)

src/status_im2/contexts/chat/menus/pinned_messages/view.cljs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
(defn pinned-messages
2323
[chat-id]
2424
(let [pinned-messages (rf/sub [:chats/pinned-sorted-list chat-id])
25-
render-data (rf/sub [:chats/current-chat-message-list-view-context :in-pinned-view])
25+
render-data (rf/sub [:chats/current-chat-message-list-view-context :in-pinned-view?])
2626
current-chat (rf/sub [:chat-by-id chat-id])
2727
{:keys [community-id]} current-chat
2828
community (rf/sub [:communities/community community-id])]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
(ns status-im2.contexts.chat.messages.content.audio.component-spec
2+
(:require [status-im2.contexts.chat.messages.content.audio.view :as audio-message]
3+
[test-helpers.component :as h]
4+
[react-native.audio-toolkit :as audio]
5+
[re-frame.core :as re-frame]))
6+
7+
(defonce message
8+
{:audio-duration-ms 5000
9+
:message-id "message-id"})
10+
11+
(defonce context
12+
{:in-pinned-view? false})
13+
14+
(defn setup-subs
15+
[subs]
16+
(doseq [keyval subs]
17+
(re-frame/reg-sub
18+
(key keyval)
19+
(fn [_] (val keyval)))))
20+
21+
(h/describe "audio message"
22+
(h/before-each
23+
#(setup-subs {:mediaserver/port 1000}))
24+
25+
(h/test "renders correctly"
26+
(h/render [audio-message/audio-message message context])
27+
(h/is-truthy (h/get-by-label-text :audio-message-container)))
28+
29+
(h/test "press play calls audio/toggle-playpause-player"
30+
(with-redefs [audio/toggle-playpause-player (js/jest.fn)
31+
audio/new-player (fn [_ _ _] {})
32+
audio/destroy-player #()
33+
audio/prepare-player (fn [_ on-success _] (on-success))
34+
audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))]
35+
(h/render [audio-message/audio-message message context])
36+
(h/fire-event
37+
:on-press
38+
(h/get-by-label-text :play-pause-audio-message-button))
39+
(-> (h/expect audio/toggle-playpause-player)
40+
(.toHaveBeenCalledTimes 1))))
41+
42+
(h/test "press play renders error"
43+
(h/render [audio-message/audio-message message context])
44+
(with-redefs [audio/toggle-playpause-player (fn [_ _ _ on-error] (on-error))
45+
audio/new-player (fn [_ _ _] {})
46+
audio/destroy-player #()
47+
audio/prepare-player (fn [_ on-success _] (on-success))
48+
audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))]
49+
(h/fire-event
50+
:on-press
51+
(h/get-by-label-text :play-pause-audio-message-button))
52+
(h/wait-for #(h/is-truthy (h/get-by-label-text :audio-error-label))))))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
(ns status-im2.contexts.chat.messages.content.audio.style
2+
(:require [quo2.foundations.colors :as colors]
3+
[quo2.theme :as theme]))
4+
5+
(defn container
6+
[]
7+
{:width 295
8+
:height 56
9+
:border-radius 12
10+
:border-width 1
11+
:padding 12
12+
:flex-direction :row
13+
:align-items :center
14+
:justify-content :space-between
15+
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80)
16+
:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80-opa-40)})
17+
18+
(def play-pause-slider-container
19+
{:flex-direction :row
20+
:align-items :center})
21+
22+
(def slider-container
23+
{:position :absolute
24+
:left 60
25+
:right 71
26+
:bottom nil})
27+
28+
(defn play-pause-container
29+
[]
30+
{:background-color (get-in colors/customization [:blue (if (theme/dark?) 60 50)])
31+
:width 32
32+
:height 32
33+
:border-radius 16
34+
:align-items :center
35+
:justify-content :center})
36+
37+
(def timestamp
38+
{:margin-left 4})
39+
40+
(def error-label
41+
{:margin-bottom 16})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
(ns status-im2.contexts.chat.messages.content.audio.view
2+
(:require ["react-native-blob-util" :default ReactNativeBlobUtil]
3+
[goog.string :as gstring]
4+
[reagent.core :as reagent]
5+
[react-native.audio-toolkit :as audio]
6+
[status-im2.contexts.chat.messages.content.audio.style :as style]
7+
[react-native.platform :as platform]
8+
[taoensso.timbre :as log]
9+
[quo2.foundations.colors :as colors]
10+
[quo2.core :as quo]
11+
[react-native.core :as rn]
12+
[utils.re-frame :as rf]
13+
[utils.i18n :as i18n]))
14+
15+
(def ^:const media-server-uri-prefix "https://localhost:")
16+
(def ^:const audio-path "/messages/audio")
17+
(def ^:const uri-param "?messageId=")
18+
19+
(defonce active-players (atom {}))
20+
(defonce audio-uris (atom {}))
21+
(defonce progress-timer (atom nil))
22+
(defonce current-player-key (reagent/atom nil))
23+
24+
(defn get-player-key
25+
[message-id in-pinned-view?]
26+
(str in-pinned-view? message-id))
27+
28+
(defn destroy-player
29+
[player-key]
30+
(when-let [player (@active-players player-key)]
31+
(audio/destroy-player player)
32+
(swap! active-players dissoc player-key)))
33+
34+
(defn update-state
35+
[state new-state]
36+
(when-not (= @state new-state)
37+
(reset! state new-state)))
38+
39+
(defn seek-player
40+
[player-key player-state value on-success]
41+
(when-let [player (@active-players player-key)]
42+
(audio/seek-player
43+
player
44+
value
45+
#(when on-success (on-success))
46+
#(update-state player-state :error))
47+
(update-state player-state :seeking)))
48+
49+
(defn download-audio-http
50+
[base64-uri on-success]
51+
(-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?}))
52+
(.fetch "GET" (str base64-uri))
53+
(.then #(on-success (.base64 ^js %)))
54+
(.catch #(log/error "could not fetch audio " base64-uri))))
55+
56+
(defn create-player
57+
[{:keys [progress-ref player-state player-key]} audio-url on-success]
58+
(download-audio-http
59+
audio-url
60+
(fn [base64-data]
61+
(let [player (audio/new-player
62+
(str "data:audio/acc;base64," base64-data)
63+
{:autoDestroy false
64+
:continuesToPlayInBackground false}
65+
(fn []
66+
(update-state player-state :ready-to-play)
67+
(reset! progress-ref 0)
68+
(when (and @progress-timer (= @current-player-key player-key))
69+
(js/clearInterval @progress-timer)
70+
(reset! progress-timer nil))))]
71+
(swap! active-players assoc player-key player)
72+
(audio/prepare-player
73+
player
74+
#(when on-success (on-success))
75+
#(update-state player-state :error)))))
76+
(update-state player-state :preparing))
77+
78+
(defn play-pause-player
79+
[{:keys [player-key player-state progress-ref message-id audio-duration-ms seeking-audio?
80+
user-interaction?]
81+
:as params}]
82+
(let [mediaserver-port (rf/sub [:mediaserver/port])
83+
audio-uri (str media-server-uri-prefix
84+
mediaserver-port
85+
audio-path
86+
uri-param
87+
message-id)
88+
player (@active-players player-key)
89+
playing? (= @player-state :playing)]
90+
(when-not playing?
91+
(reset! current-player-key player-key))
92+
(if (and player
93+
(= (@audio-uris player-key) audio-uri))
94+
(audio/toggle-playpause-player
95+
player
96+
(fn []
97+
(update-state player-state :playing)
98+
(when @progress-timer
99+
(js/clearInterval @progress-timer))
100+
(reset! progress-timer
101+
(js/setInterval
102+
(fn []
103+
(let [player (@active-players player-key)
104+
current-time (audio/get-player-current-time player)
105+
playing? (= @player-state :playing)]
106+
(when (and playing? (not @seeking-audio?) (> current-time 0))
107+
(reset! progress-ref current-time))))
108+
100)))
109+
(fn []
110+
(update-state player-state :ready-to-play)
111+
(when (and @progress-timer user-interaction?)
112+
(js/clearInterval @progress-timer)
113+
(reset! progress-timer nil)))
114+
#(update-state player-state :error))
115+
(do
116+
(swap! audio-uris assoc player-key audio-uri)
117+
(destroy-player player-key)
118+
(create-player params
119+
audio-uri
120+
(fn []
121+
(reset! seeking-audio? false)
122+
(if (> @progress-ref 0)
123+
(let [seek-time (* audio-duration-ms @progress-ref)
124+
checked-seek-time (min audio-duration-ms seek-time)]
125+
(seek-player
126+
player-key
127+
player-state
128+
checked-seek-time
129+
#(play-pause-player params)))
130+
(play-pause-player params))))))))
131+
132+
(defn audio-message
133+
[{:keys [audio-duration-ms message-id]}
134+
{:keys [in-pinned-view?]}]
135+
(let [player-state (reagent/atom :not-loaded)
136+
progress (reagent/atom 0)
137+
seeking-audio? (reagent/atom false)
138+
player-key (get-player-key message-id in-pinned-view?)]
139+
[:f>
140+
(fn []
141+
(let [player (@active-players player-key)
142+
duration (if (and player (not (#{:preparing :not-loaded :error} @player-state)))
143+
(audio/get-player-duration player)
144+
audio-duration-ms)
145+
time-secs (quot
146+
(if (or @seeking-audio? (#{:playing :seeking} @player-state))
147+
(if (<= @progress 1) (* duration @progress) @progress)
148+
duration)
149+
1000)]
150+
(rn/use-effect (fn [] #(destroy-player player-key)))
151+
(rn/use-effect
152+
(fn []
153+
(when (and (some? @current-player-key)
154+
(not= @current-player-key player-key)
155+
(= @player-state :playing))
156+
(play-pause-player {:player-key player-key
157+
:player-state player-state
158+
:progress-ref progress
159+
:message-id message-id
160+
:audio-duration-ms duration
161+
:seeking-audio? seeking-audio?
162+
:user-interaction? false})))
163+
[@current-player-key])
164+
(if (= @player-state :error)
165+
[quo/text
166+
{:style style/error-label
167+
:accessibility-label :audio-error-label
168+
:weight :medium
169+
:size :paragraph-2}
170+
(i18n/label :error-loading-audio)]
171+
[rn/view
172+
{:accessibility-label :audio-message-container
173+
:style (style/container)}
174+
[rn/touchable-opacity
175+
{:accessibility-label :play-pause-audio-message-button
176+
:on-press #(play-pause-player {:player-key player-key
177+
:player-state player-state
178+
:progress-ref progress
179+
:message-id message-id
180+
:audio-duration-ms duration
181+
:seeking-audio? seeking-audio?
182+
:user-interaction? true})
183+
:style (style/play-pause-container)}
184+
[quo/icon
185+
(case @player-state
186+
:preparing :i/loading
187+
:playing :i/pause-audio
188+
:i/play-audio)
189+
{:size 20
190+
:color colors/white}]]
191+
[quo/soundtrack
192+
{:style style/slider-container
193+
:audio-current-time-ms progress
194+
:player-ref (@active-players player-key)
195+
:seeking-audio? seeking-audio?}]
196+
[quo/text
197+
{:style style/timestamp
198+
:accessibility-label :audio-duration-label
199+
:weight :medium
200+
:size :paragraph-2}
201+
(gstring/format "%02d:%02d" (quot time-secs 60) (mod time-secs 60))]])))]))

0 commit comments

Comments
 (0)