feat: audio component
Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
parent
0226a92c07
commit
e5913cc3ea
Binary file not shown.
After Width: | Height: | Size: 271 B |
Binary file not shown.
After Width: | Height: | Size: 367 B |
Binary file not shown.
After Width: | Height: | Size: 363 B |
Binary file not shown.
After Width: | Height: | Size: 524 B |
|
@ -522,7 +522,7 @@
|
|||
seeking-audio?]
|
||||
[:f> soundtrack/f-soundtrack
|
||||
{:audio-current-time-ms audio-current-time-ms
|
||||
:player-ref player-ref
|
||||
:player-ref @player-ref
|
||||
:seeking-audio? seeking-audio?}]])
|
||||
(when (or @recording? @reviewing-audio?)
|
||||
[:f> f-time-counter @recording? @recording-length-ms @ready-to-delete? @reviewing-audio?
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
(let [player-ref (reagent/atom {})
|
||||
audio-current-time-ms (reagent/atom 0)]
|
||||
(h/render [:f> soundtrack/f-soundtrack
|
||||
{:player-ref player-ref
|
||||
{:player-ref @player-ref
|
||||
:audio-current-time-ms audio-current-time-ms}])
|
||||
(-> (h/expect (h/get-by-test-id "soundtrack"))
|
||||
(.toBeTruthy)))))
|
||||
|
@ -31,7 +31,7 @@
|
|||
audio-current-time-ms (reagent/atom 0)]
|
||||
(h/render [:f> soundtrack/f-soundtrack
|
||||
{:seeking-audio? seeking-audio?
|
||||
:player-ref player-ref
|
||||
:player-ref @player-ref
|
||||
:audio-current-time-ms audio-current-time-ms}])
|
||||
(h/fire-event
|
||||
:on-sliding-start
|
||||
|
@ -47,7 +47,7 @@
|
|||
audio-current-time-ms (reagent/atom 0)]
|
||||
(h/render [:f> soundtrack/f-soundtrack
|
||||
{:seeking-audio? seeking-audio?
|
||||
:player-ref player-ref
|
||||
:player-ref @player-ref
|
||||
:audio-current-time-ms audio-current-time-ms}])
|
||||
(h/fire-event
|
||||
:on-sliding-start
|
||||
|
@ -69,7 +69,7 @@
|
|||
audio-current-time-ms (reagent/atom 0)]
|
||||
(h/render [:f> soundtrack/f-soundtrack
|
||||
{:seeking-audio? seeking-audio?
|
||||
:player-ref player-ref
|
||||
:player-ref @player-ref
|
||||
:audio-current-time-ms audio-current-time-ms}])
|
||||
(h/fire-event
|
||||
:on-sliding-start
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png"))
|
||||
|
||||
(defn f-soundtrack
|
||||
[{:keys [audio-current-time-ms player-ref seeking-audio?]}]
|
||||
(let [audio-duration-ms (audio/get-player-duration @player-ref)]
|
||||
[{:keys [audio-current-time-ms player-ref style seeking-audio?]}]
|
||||
(let [audio-duration-ms (audio/get-player-duration player-ref)]
|
||||
[:<>
|
||||
[slider/slider
|
||||
{:test-ID "soundtrack"
|
||||
:style (style/player-slider-container)
|
||||
:style (merge
|
||||
(style/player-slider-container)
|
||||
(or style {}))
|
||||
:minimum-value 0
|
||||
:maximum-value audio-duration-ms
|
||||
:value @audio-current-time-ms
|
||||
|
@ -23,7 +25,7 @@
|
|||
:on-sliding-complete (fn [seek-time]
|
||||
(reset! seeking-audio? false)
|
||||
(audio/seek-player
|
||||
@player-ref
|
||||
player-ref
|
||||
seek-time
|
||||
#(log/debug "[record-audio] on seek - seek time: " seek-time)
|
||||
#(log/error "[record-audio] on seek - error: " %)))
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
quo2.components.profile.select-profile.view
|
||||
quo2.components.reactions.reaction
|
||||
quo2.components.record-audio.record-audio.view
|
||||
quo2.components.record-audio.soundtrack.view
|
||||
quo2.components.selectors.disclaimer.view
|
||||
quo2.components.selectors.filter.view
|
||||
quo2.components.selectors.selectors.view
|
||||
|
@ -186,6 +187,7 @@
|
|||
|
||||
;;;; RECORD AUDIO
|
||||
(def record-audio quo2.components.record-audio.record-audio.view/record-audio)
|
||||
(def soundtrack quo2.components.record-audio.soundtrack.view/f-soundtrack)
|
||||
|
||||
;;;; SETTINGS
|
||||
(def privacy-option quo2.components.settings.privacy-option/card)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
(ns status-im2.contexts.chat.messages.content.audio.component-spec
|
||||
(:require [status-im2.contexts.chat.messages.content.audio.view :as audio-message]
|
||||
[test-helpers.component :as h]
|
||||
[react-native.audio-toolkit :as audio]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(def message
|
||||
{:audio-duration-ms 5000
|
||||
:message-id "message-id"})
|
||||
|
||||
(def context
|
||||
{:in-pinned-view? false})
|
||||
|
||||
(defn setup-subs
|
||||
[subs]
|
||||
(doseq [keyval subs]
|
||||
(re-frame/reg-sub
|
||||
(key keyval)
|
||||
(fn [_] (val keyval)))))
|
||||
|
||||
(h/describe "audio message"
|
||||
(h/before-each
|
||||
#(setup-subs {:mediaserver/port 1000}))
|
||||
|
||||
(h/test "renders correctly"
|
||||
(h/render [audio-message/audio-message message context])
|
||||
(h/is-truthy (h/get-by-label-text :audio-message-container)))
|
||||
|
||||
(h/test "press play calls audio/toggle-playpause-player"
|
||||
(with-redefs [audio/toggle-playpause-player (js/jest.fn)
|
||||
audio/new-player (fn [_ _ _] {})
|
||||
audio/destroy-player #()
|
||||
audio/prepare-player (fn [_ on-success _] (on-success))
|
||||
audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))]
|
||||
(h/render [audio-message/audio-message message context])
|
||||
(h/fire-event
|
||||
:on-press
|
||||
(h/get-by-label-text :play-pause-audio-message-button))
|
||||
(-> (h/expect audio/toggle-playpause-player)
|
||||
(.toHaveBeenCalledTimes 1))))
|
||||
|
||||
(h/test "press play renders error"
|
||||
(h/render [audio-message/audio-message message context])
|
||||
(with-redefs [audio/toggle-playpause-player (fn [_ _ _ on-error] (on-error))
|
||||
audio/new-player (fn [_ _ _] {})
|
||||
audio/destroy-player #()
|
||||
audio/prepare-player (fn [_ on-success _] (on-success))
|
||||
audio-message/download-audio-http (fn [_ on-success] (on-success "audio-uri"))]
|
||||
(h/fire-event
|
||||
:on-press
|
||||
(h/get-by-label-text :play-pause-audio-message-button))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :audio-error-label))))))
|
|
@ -0,0 +1,41 @@
|
|||
(ns status-im2.contexts.chat.messages.content.audio.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[quo2.theme :as theme]))
|
||||
|
||||
(defn container
|
||||
[]
|
||||
{:width 295
|
||||
:height 56
|
||||
:border-radius 12
|
||||
:border-width 1
|
||||
:padding 12
|
||||
:flex-direction :row
|
||||
:align-items :center
|
||||
:justify-content :space-between
|
||||
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80)
|
||||
:background-color (colors/theme-colors colors/neutral-5 colors/neutral-80-opa-40)})
|
||||
|
||||
(def play-pause-slider-container
|
||||
{:flex-direction :row
|
||||
:align-items :center})
|
||||
|
||||
(def slider-container
|
||||
{:position :absolute
|
||||
:left 60
|
||||
:right 71
|
||||
:bottom nil})
|
||||
|
||||
(defn play-pause-container
|
||||
[]
|
||||
{:background-color (get-in colors/customization [:blue (if (theme/dark?) 60 50)])
|
||||
:width 32
|
||||
:height 32
|
||||
:border-radius 16
|
||||
:align-items :center
|
||||
:justify-content :center})
|
||||
|
||||
(def timestamp
|
||||
{:margin-left 4})
|
||||
|
||||
(def error-label
|
||||
{:margin-bottom 16})
|
|
@ -0,0 +1,204 @@
|
|||
(ns status-im2.contexts.chat.messages.content.audio.view
|
||||
(:require ["react-native-blob-util" :default ReactNativeBlobUtil]
|
||||
[goog.string :as gstring]
|
||||
[reagent.core :as reagent]
|
||||
[react-native.audio-toolkit :as audio]
|
||||
[status-im2.contexts.chat.messages.content.audio.style :as style]
|
||||
[react-native.platform :as platform]
|
||||
[taoensso.timbre :as log]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.i18n :as i18n]))
|
||||
|
||||
(def ^:const media-server-uri-prefix "https://localhost:")
|
||||
(def ^:const audio-path "/messages/audio")
|
||||
(def ^:const uri-param "?messageId=")
|
||||
|
||||
(defonce active-players (atom {}))
|
||||
(defonce audio-uris (atom {}))
|
||||
(defonce progress-timer (atom nil))
|
||||
(defonce current-player-key (reagent/atom nil))
|
||||
|
||||
(defn get-player-key
|
||||
[message-id in-pinned-view?]
|
||||
(str in-pinned-view? message-id))
|
||||
|
||||
(defn destroy-player
|
||||
[player-key]
|
||||
(when-let [player (@active-players player-key)]
|
||||
(audio/destroy-player player)
|
||||
(swap! active-players dissoc player-key)))
|
||||
|
||||
(defn update-state
|
||||
[state new-state]
|
||||
(when-not (= @state new-state)
|
||||
(reset! state new-state)))
|
||||
|
||||
(defn seek-player
|
||||
[player-key player-state value on-success]
|
||||
(when-let [player (@active-players player-key)]
|
||||
(audio/seek-player
|
||||
player
|
||||
value
|
||||
#(when on-success (on-success))
|
||||
#(update-state player-state :error))
|
||||
(update-state player-state :seeking)))
|
||||
|
||||
(defn download-audio-http
|
||||
[base64-uri on-success]
|
||||
(-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?}))
|
||||
(.fetch "GET" (str base64-uri))
|
||||
(.then #(on-success (.base64 ^js %)))
|
||||
(.catch #(log/error "could not fetch audio " base64-uri))))
|
||||
|
||||
(defn create-player
|
||||
[{:keys [progress player-state player-key]} audio-url on-success]
|
||||
(download-audio-http
|
||||
audio-url
|
||||
(fn [base64-data]
|
||||
(let [player (audio/new-player
|
||||
(str "data:audio/acc;base64," base64-data)
|
||||
{:autoDestroy false
|
||||
:continuesToPlayInBackground false}
|
||||
(fn []
|
||||
(update-state player-state :ready-to-play)
|
||||
(reset! progress 0)
|
||||
(when (and @progress-timer (= @current-player-key player-key))
|
||||
(js/clearInterval @progress-timer)
|
||||
(reset! progress-timer nil))))]
|
||||
(swap! active-players assoc player-key player)
|
||||
(audio/prepare-player
|
||||
player
|
||||
#(when on-success (on-success))
|
||||
#(update-state player-state :error)))))
|
||||
(update-state player-state :preparing))
|
||||
|
||||
(defn play-pause-player
|
||||
[{:keys [player-key player-state progress message-id audio-duration-ms seeking-audio?
|
||||
user-interaction?]
|
||||
:as params}]
|
||||
(let [mediaserver-port (rf/sub [:mediaserver/port])
|
||||
audio-uri (str media-server-uri-prefix
|
||||
mediaserver-port
|
||||
audio-path
|
||||
uri-param
|
||||
message-id)
|
||||
player (@active-players player-key)
|
||||
playing? (= @player-state :playing)]
|
||||
(when-not playing?
|
||||
(reset! current-player-key player-key))
|
||||
(if (and player
|
||||
(= (@audio-uris player-key) audio-uri))
|
||||
(audio/toggle-playpause-player
|
||||
player
|
||||
(fn []
|
||||
(update-state player-state :playing)
|
||||
(when @progress-timer
|
||||
(js/clearInterval @progress-timer))
|
||||
(reset! progress-timer
|
||||
(js/setInterval
|
||||
(fn []
|
||||
(let [player (@active-players player-key)
|
||||
current-time (audio/get-player-current-time player)
|
||||
playing? (= @player-state :playing)]
|
||||
(when (and playing? (not @seeking-audio?) (> current-time 0))
|
||||
(reset! progress current-time))))
|
||||
100)))
|
||||
(fn []
|
||||
(update-state player-state :ready-to-play)
|
||||
(when (and @progress-timer user-interaction?)
|
||||
(js/clearInterval @progress-timer)
|
||||
(reset! progress-timer nil)))
|
||||
#(update-state player-state :error))
|
||||
(do
|
||||
(swap! audio-uris assoc player-key audio-uri)
|
||||
(destroy-player player-key)
|
||||
(create-player params
|
||||
audio-uri
|
||||
(fn []
|
||||
(reset! seeking-audio? false)
|
||||
(if (> @progress 0)
|
||||
(let [seek-time (* audio-duration-ms @progress)
|
||||
checked-seek-time (min audio-duration-ms seek-time)]
|
||||
(seek-player
|
||||
player-key
|
||||
player-state
|
||||
checked-seek-time
|
||||
#(play-pause-player params)))
|
||||
(play-pause-player params))))))))
|
||||
|
||||
(defn f-audio-message
|
||||
[player-state progress seeking-audio? {:keys [audio-duration-ms message-id]}
|
||||
{:keys [in-pinned-view?]}]
|
||||
(let [player-key (get-player-key message-id in-pinned-view?)
|
||||
player (@active-players player-key)
|
||||
duration (if (and player (not (#{:preparing :not-loaded :error} @player-state)))
|
||||
(audio/get-player-duration player)
|
||||
audio-duration-ms)
|
||||
time-secs (quot
|
||||
(if (or @seeking-audio? (#{:playing :seeking} @player-state))
|
||||
(if (<= @progress 1) (* duration @progress) @progress)
|
||||
duration)
|
||||
1000)]
|
||||
(rn/use-effect (fn [] #(destroy-player player-key)))
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (and (some? @current-player-key)
|
||||
(not= @current-player-key player-key)
|
||||
(= @player-state :playing))
|
||||
(play-pause-player {:player-key player-key
|
||||
:player-state player-state
|
||||
:progress progress
|
||||
:message-id message-id
|
||||
:audio-duration-ms duration
|
||||
:seeking-audio? seeking-audio?
|
||||
:user-interaction? false})))
|
||||
[@current-player-key])
|
||||
(if (= @player-state :error)
|
||||
[quo/text
|
||||
{:style style/error-label
|
||||
:accessibility-label :audio-error-label
|
||||
:weight :medium
|
||||
:size :paragraph-2}
|
||||
(i18n/label :error-loading-audio)]
|
||||
[rn/view
|
||||
{:accessibility-label :audio-message-container
|
||||
:style (style/container)}
|
||||
[rn/touchable-opacity
|
||||
{:accessibility-label :play-pause-audio-message-button
|
||||
:on-press #(play-pause-player {:player-key player-key
|
||||
:player-state player-state
|
||||
:progress progress
|
||||
:message-id message-id
|
||||
:audio-duration-ms duration
|
||||
:seeking-audio? seeking-audio?
|
||||
:user-interaction? true})
|
||||
:style (style/play-pause-container)}
|
||||
[quo/icon
|
||||
(case @player-state
|
||||
:preparing :i/loading
|
||||
:playing :i/pause-audio
|
||||
:i/play-audio)
|
||||
{:size 20
|
||||
:color colors/white}]]
|
||||
[:f> quo/soundtrack
|
||||
{:style style/slider-container
|
||||
:audio-current-time-ms progress
|
||||
:player-ref (@active-players player-key)
|
||||
:seeking-audio? seeking-audio?}]
|
||||
[quo/text
|
||||
{:style style/timestamp
|
||||
:accessibility-label :audio-duration-label
|
||||
:weight :medium
|
||||
:size :paragraph-2}
|
||||
(gstring/format "%02d:%02d" (quot time-secs 60) (mod time-secs 60))]])))
|
||||
|
||||
(defn audio-message
|
||||
[message context]
|
||||
(let [player-state (reagent/atom :not-loaded)
|
||||
progress (reagent/atom 0)
|
||||
seeking-audio? (reagent/atom false)]
|
||||
(fn []
|
||||
[:f> f-audio-message player-state progress seeking-audio? message context])))
|
|
@ -14,6 +14,7 @@
|
|||
[status-im2.contexts.chat.messages.content.album.view :as album]
|
||||
[status-im2.contexts.chat.messages.avatar.view :as avatar]
|
||||
[status-im2.contexts.chat.messages.content.image.view :as image]
|
||||
[status-im2.contexts.chat.messages.content.audio.view :as audio]
|
||||
[quo2.core :as quo]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im.ui2.screens.chat.messages.message :as old-message]
|
||||
|
@ -128,7 +129,7 @@
|
|||
[not-implemented/not-implemented [old-message/sticker message-data]]
|
||||
|
||||
constants/content-type-audio
|
||||
[not-implemented/not-implemented [old-message/audio message-data]]
|
||||
[audio/audio-message message-data context]
|
||||
|
||||
constants/content-type-image
|
||||
[image/image-message 0 message-data context on-long-press]
|
||||
|
|
|
@ -2,19 +2,28 @@
|
|||
(:require [quo2.core :as quo]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.messages.resolver.message-resolver :as resolver]))
|
||||
[status-im2.contexts.chat.messages.resolver.message-resolver :as resolver]
|
||||
[status-im2.constants :as constants]))
|
||||
|
||||
(defn message-text
|
||||
[{:keys [content-type] :as message}]
|
||||
(cond (= content-type constants/content-type-audio)
|
||||
(i18n/label :audio-message)
|
||||
:else
|
||||
(get-in message [:content :parsed-text])))
|
||||
|
||||
(defn banner
|
||||
[chat-id]
|
||||
(let [pinned-message (rf/sub [:chats/last-pinned-message chat-id])
|
||||
latest-pin-text (get-in pinned-message [:content :parsed-text])
|
||||
latest-pin-text (message-text pinned-message)
|
||||
{:keys [deleted? deleted-for-me?]} pinned-message
|
||||
pins-count (rf/sub [:chats/pin-messages-count chat-id])
|
||||
|
||||
content-type-text? (= (:content-type pinned-message) constants/content-type-text)
|
||||
latest-pin-text
|
||||
(cond deleted? (i18n/label :t/message-deleted-for-everyone)
|
||||
deleted-for-me? (i18n/label :t/message-deleted-for-you)
|
||||
:else (resolver/resolve-message latest-pin-text))]
|
||||
content-type-text? (resolver/resolve-message latest-pin-text)
|
||||
:else latest-pin-text)]
|
||||
[quo/banner
|
||||
{:latest-pin-text latest-pin-text
|
||||
:pins-count pins-count
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
(ns status-im2.core-spec
|
||||
(:require
|
||||
[status-im2.contexts.communities.actions.community-options.component-spec]))
|
||||
[status-im2.contexts.communities.actions.community-options.component-spec]
|
||||
[status-im2.contexts.chat.messages.content.audio.component-spec]))
|
||||
|
|
|
@ -63,6 +63,11 @@
|
|||
|
||||
(def within rtl/within)
|
||||
|
||||
(defn wait-for
|
||||
([condition] (wait-for condition {}))
|
||||
([condition options]
|
||||
(rtl/waitFor condition (clj->js options))))
|
||||
|
||||
(defn fire-event
|
||||
([event-name node]
|
||||
(fire-event event-name node nil))
|
||||
|
|
|
@ -70,6 +70,14 @@ jest.mock("i18n-js", () => ({
|
|||
t: (label) => `tx:${label}`
|
||||
}));
|
||||
|
||||
jest.mock("react-native-blob-util", () => ({
|
||||
default: {
|
||||
config: jest.fn().mockReturnValue({
|
||||
fetch: jest.fn()
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
NativeModules.ReactLocalization = {
|
||||
language: 'en',
|
||||
locale: 'en',
|
||||
|
|
|
@ -2087,5 +2087,6 @@
|
|||
"community-request-pending": "Request pending",
|
||||
"community-request-pending-body-text": "You requested to join",
|
||||
"community-kicked-heading": "Kicked from community",
|
||||
"community-kicked-body": "You were kicked from"
|
||||
"community-kicked-body": "You were kicked from",
|
||||
"error-loading-audio": "Error while loading audio"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue