[#16755] - Network routing component (#17457)

* Add network routing component & animations
* Add basic tests and accessibility labels
This commit is contained in:
Ulises Manuel 2023-10-26 16:07:44 -06:00 committed by GitHub
parent c9b3196203
commit 1f71883fa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 509 additions and 0 deletions

View File

@ -0,0 +1,115 @@
(ns quo.components.wallet.network-routing.animation
(:require [react-native.reanimated :as reanimated]))
(def ^:private slider-timing 300)
(defn show-slider
[opacity-shared-value]
(reanimated/animate opacity-shared-value 1 slider-timing))
(defn hide-slider
[opacity-shared-value]
(reanimated/animate opacity-shared-value 0 slider-timing))
(defn increase-slider
[width-shared-value height-shared-value]
(reanimated/animate width-shared-value 8 slider-timing)
(reanimated/animate height-shared-value 40 slider-timing))
(defn decrease-slider
[width-shared-value height-shared-value]
(reanimated/animate width-shared-value 4 slider-timing)
(reanimated/animate height-shared-value 32 slider-timing))
(def ^:private pressed-bar-timing 600)
(defn move-previous-bars
[{:keys [bars bars-widths-negative]}]
(doseq [[bar-idx bar] (map-indexed vector bars)
:let [new-translation-x (->> (take (inc bar-idx) bars-widths-negative)
(reduce +)
(dec))]]
(reanimated/animate (:translate-x-shared-value bar)
new-translation-x
pressed-bar-timing)))
(defn move-pressed-bar
[{:keys [bars-widths-negative number-previous-bars]
{:keys [translate-x-shared-value]} :bar}]
(let [new-translation-x (reduce + (take number-previous-bars bars-widths-negative))]
(reanimated/animate translate-x-shared-value
new-translation-x
(- pressed-bar-timing 20))))
(defn move-next-bars
[{:keys [bars bars-widths-negative number-previous-bars extra-offset add-new-timeout]}]
(doseq [[bar-idx bar] (map-indexed vector bars)
:let [number-bars-before (+ number-previous-bars
(inc bar-idx))
new-translation-x (->> (take number-bars-before bars-widths-negative)
(reduce +)
(* 1.05))]]
(reanimated/animate (:translate-x-shared-value bar) new-translation-x pressed-bar-timing)
(add-new-timeout
(keyword (str "fix-next-bars-position-" bar-idx))
(fn []
(let [translate-x-value (reanimated/get-shared-value (:translate-x-shared-value bar))
hidden-position (- translate-x-value extra-offset)]
(reanimated/set-shared-value (:translate-x-shared-value bar) hidden-position)))
pressed-bar-timing)))
(def ^:private max-limit-bar-timing 300)
(defn show-max-limit-bar
[max-limit-bar-opacity]
(reanimated/animate max-limit-bar-opacity 1 max-limit-bar-timing))
(defn hide-max-limit-bar
[max-limit-bar-opacity]
(reanimated/animate max-limit-bar-opacity 0 max-limit-bar-timing))
(defn reset-bars-positions
[bars unlock-press-fn add-new-timeout]
(let [bars-reset-timing 500]
(doseq [{:keys [translate-x-shared-value]} bars]
(reanimated/animate translate-x-shared-value 0 bars-reset-timing))
(add-new-timeout :unlock-press unlock-press-fn bars-reset-timing)))
(defn align-bars-off-screen
[{:keys [new-network-values network-bars amount->width add-new-timeout]}]
(let [width-to-off-screen (->> new-network-values
(reduce #(- %1 (:amount %2)) 0)
(amount->width))]
(doseq [[bar-idx {new-amount :amount} bar] (map vector (range) new-network-values network-bars)]
(reanimated/set-shared-value (:amount-shared-value bar) new-amount)
(reanimated/set-shared-value (:translate-x-shared-value bar) (* 2 width-to-off-screen))
(add-new-timeout
(keyword (str "align-bar-" bar-idx))
#(reanimated/set-shared-value (:translate-x-shared-value bar) width-to-off-screen)
1))))
(def ^:private hide-bar-timing 400)
(defn hide-pressed-bar
[{:keys [translate-x-shared-value amount-shared-value]} amount->width]
(let [bar-width (amount->width (reanimated/get-shared-value amount-shared-value))
new-translation-x (- (reanimated/get-shared-value translate-x-shared-value)
bar-width)]
(reanimated/animate translate-x-shared-value new-translation-x hide-bar-timing)))
(defn update-bar-values-and-reset-animations
[{:keys [new-network-values network-bars amount->width reset-values-fn add-new-timeout
lock-press-fn unlock-press-fn]}]
(lock-press-fn)
(add-new-timeout
:update-bars-values
(fn []
(align-bars-off-screen {:new-network-values new-network-values
:network-bars network-bars
:amount->width amount->width
:add-new-timeout add-new-timeout})
(reset-values-fn)
(add-new-timeout :reset-bars
#(reset-bars-positions network-bars unlock-press-fn add-new-timeout)
100))
hide-bar-timing))

View File

@ -0,0 +1,79 @@
(ns quo.components.wallet.network-routing.style
(:require [quo.foundations.colors :as colors]
[react-native.reanimated :as reanimated]))
(defn container
[container-style theme]
(assoc container-style
:flex-direction :row
:height 64
:background-color (colors/theme-colors colors/neutral-100-opa-5 colors/neutral-90 theme)
:border-radius 20
:overflow :hidden))
(defn max-limit-bar
[{:keys [opacity-shared-value width]}]
(reanimated/apply-animations-to-style
{:opacity opacity-shared-value}
{:position :absolute
:top 0
:bottom 0
:left 0
:width width
:z-index -1
:flex-direction :row}))
(defn max-limit-bar-background
[network-name]
{:flex 1
:background-color (colors/resolve-color network-name nil 10)})
(defn network-bar
[{:keys [max-width on-top? bar-division? theme]
{:keys [network-name translate-x-shared-value]} :bar}
width-shared-value]
(reanimated/apply-animations-to-style
{:width width-shared-value
:transform [{:translate-x translate-x-shared-value}]}
{:max-width max-width
:flex-direction :row
:justify-content :flex-end
:background-color (colors/resolve-color network-name nil)
:z-index (if on-top? 1 0)
:border-right-width (if bar-division? 0 1)
:border-color (colors/theme-colors colors/white colors/neutral-95 theme)}))
(def slider-container
{:width 40
:background-color :transparent
:justify-content :center
:align-items :center
:right -20})
(def ^:private slider-fixed-styles
{:background-color colors/white
:height 32
:width 4
:border-radius 4})
(defn slider
[{:keys [width-shared-value height-shared-value opacity-shared-value]}]
(reanimated/apply-animations-to-style
{:width width-shared-value
:height height-shared-value
:opacity opacity-shared-value}
slider-fixed-styles))
(def dashed-line
{:width 1
:height "100%"
:margin-left -1
:margin-top -1.5})
(defn dashed-line-line
[network-name]
{:background-color (colors/resolve-color network-name nil)
:height 3
:width 1})
(def dashed-line-space {:height 4 :width 1})

View File

@ -0,0 +1,189 @@
(ns quo.components.wallet.network-routing.view
(:require
[oops.core :as oops]
[quo.components.wallet.network-routing.animation :as animation]
[quo.components.wallet.network-routing.style :as style]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.gesture :as gesture]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[utils.number]))
(def ^:private timeouts (atom {}))
(defn- add-new-timeout
[k f ms]
(letfn [(exec-fn-and-remove-timeout []
(f)
(swap! timeouts dissoc k))]
(js/clearTimeout (k @timeouts))
(swap! timeouts assoc k (js/setTimeout exec-fn-and-remove-timeout ms))))
(defn- f-slider
[slider-shared-values]
[rn/view {:style style/slider-container}
[reanimated/view {:style (style/slider slider-shared-values)}]])
(defn f-network-bar
[_]
(let [detecting-gesture? (reagent/atom false)
amount-on-gesture-start (atom 0)]
(fn [{:keys [total-width total-amount on-press on-new-amount allow-press?]
{:keys [amount-shared-value
max-amount]} :bar
:as props}]
(let [slider-width-shared-value (reanimated/use-shared-value 4)
slider-height-shared-value (reanimated/use-shared-value 32)
slider-opacity-shared-value (reanimated/use-shared-value 0)
network-bar-shared-value (reanimated/interpolate amount-shared-value
[0 total-amount]
[0 total-width])
width->amount #(/ (* % total-amount) total-width)]
[rn/touchable-without-feedback
{:on-press (fn []
(when (and (not @detecting-gesture?) allow-press?)
(on-press)
(reset! detecting-gesture? true)
(animation/show-slider slider-opacity-shared-value)))}
[reanimated/view
{:style (style/network-bar props network-bar-shared-value)
:accessibility-label :network-routing-bar}
[gesture/gesture-detector
{:gesture
(-> (gesture/gesture-pan)
(gesture/enabled @detecting-gesture?)
(gesture/on-begin
(fn [_]
(animation/increase-slider slider-width-shared-value slider-height-shared-value)
(reset! amount-on-gesture-start (reanimated/get-shared-value amount-shared-value))))
(gesture/on-update
(fn [event]
(let [new-amount (-> (oops/oget event "translationX")
(width->amount)
(+ @amount-on-gesture-start)
(utils.number/value-in-range 1 max-amount))]
(reanimated/set-shared-value amount-shared-value new-amount))))
(gesture/on-finalize
(fn [_]
(animation/decrease-slider slider-width-shared-value slider-height-shared-value)
(animation/hide-slider slider-opacity-shared-value)
(on-new-amount (reanimated/get-shared-value amount-shared-value))
(add-new-timeout :turn-off-gesture #(reset! detecting-gesture? false) 20))))}
[:f> f-slider
{:width-shared-value slider-width-shared-value
:height-shared-value slider-height-shared-value
:opacity-shared-value slider-opacity-shared-value}]]]]))))
(defn- add-bar-shared-values
[{:keys [amount] :as network}]
(assoc network
:amount-shared-value (reanimated/use-shared-value amount)
:translate-x-shared-value (reanimated/use-shared-value 0)))
(def ^:private get-negative-amount
(comp - reanimated/get-shared-value :amount-shared-value))
(defn- dashed-line
[network-name]
[rn/view {:style style/dashed-line}
(take 19
(interleave (repeat [rn/view {:style (style/dashed-line-line network-name)}])
(repeat [rn/view {:style style/dashed-line-space}])))])
(defn f-network-routing-bars
[_]
(let [selected-network-idx (reagent/atom nil)
press-locked? (reagent/atom false)
lock-press #(reset! press-locked? true)
unlock-press #(reset! press-locked? false)
reset-state-values #(reset! selected-network-idx nil)]
(fn [{:keys [networks total-width total-amount requesting-data? on-amount-selected]}]
(let [bar-opacity-shared-value (reanimated/use-shared-value 0)
network-bars (map add-bar-shared-values networks)
amount->width #(* % (/ total-width total-amount))
bars-widths-negative (map #(-> % get-negative-amount amount->width)
network-bars)
last-bar-idx (dec (count network-bars))]
(rn/use-effect
#(when (and (not requesting-data?) @selected-network-idx)
(let [bar (nth network-bars @selected-network-idx)]
(animation/hide-pressed-bar bar amount->width))
(animation/update-bar-values-and-reset-animations
{:new-network-values networks
:network-bars network-bars
:amount->width amount->width
:reset-values-fn reset-state-values
:lock-press-fn lock-press
:unlock-press-fn unlock-press
:add-new-timeout add-new-timeout}))
[requesting-data?])
[:<>
(doall
(for [[bar-idx bar] (map-indexed vector network-bars)
:let [bar-max-width (amount->width (:max-amount bar))
bar-width (-> (:amount-shared-value bar)
(reanimated/get-shared-value)
(amount->width))
hide-division? (or (= last-bar-idx bar-idx) @selected-network-idx)
this-bar-selected? (= @selected-network-idx bar-idx)]]
^{:key (str "network-bar-" bar-idx)}
[:f> f-network-bar
{:bar bar
:max-width bar-max-width
:total-width total-width
:total-amount total-amount
:bar-division? hide-division?
:on-top? this-bar-selected?
:allow-press? (and (or (not @selected-network-idx) this-bar-selected?)
(not requesting-data?)
(not @press-locked?))
:on-press (fn []
(when-not @selected-network-idx
(let [[previous-bars [_ & next-bars]] (split-at bar-idx network-bars)
number-previous-bars bar-idx]
(animation/move-previous-bars
{:bars previous-bars
:bars-widths-negative bars-widths-negative})
(animation/move-pressed-bar
{:bar bar
:bars-widths-negative bars-widths-negative
:number-previous-bars number-previous-bars})
(animation/move-next-bars
{:bars next-bars
:bars-widths-negative bars-widths-negative
:number-previous-bars (inc number-previous-bars)
:extra-offset (max 0 (- bar-max-width bar-width))
:add-new-timeout add-new-timeout}))
(animation/show-max-limit-bar bar-opacity-shared-value)
(reset! selected-network-idx bar-idx)))
:on-new-amount (fn [new-amount]
(animation/hide-max-limit-bar bar-opacity-shared-value)
(when on-amount-selected
(on-amount-selected new-amount @selected-network-idx)))}]))
(let [{:keys [max-amount network-name]} (some->> @selected-network-idx
(nth network-bars))
limit-bar-width (amount->width max-amount)]
[reanimated/view
{:style (style/max-limit-bar
{:opacity-shared-value bar-opacity-shared-value
:width limit-bar-width})}
[rn/view {:style (style/max-limit-bar-background network-name)}]
[dashed-line network-name]])]))))
(defn view-internal
[{:keys [networks container-style theme] :as params}]
(reagent/with-let [total-width (reagent/atom nil)]
[rn/view
{:accessibility-label :network-routing
:style (style/container container-style theme)
:on-layout #(reset! total-width (oops/oget % "nativeEvent.layout.width"))}
(when @total-width
^{:key (str "network-routing-" (count networks))}
[:f> f-network-routing-bars (assoc params :total-width @total-width)])]
(finally
(doseq [[_ living-timeout] @timeouts]
(js/clearTimeout living-timeout)))))
(def view (quo.theme/with-theme view-internal))

View File

@ -141,6 +141,7 @@
quo.components.wallet.network-amount.view
quo.components.wallet.network-bridge.view
quo.components.wallet.network-link.view
quo.components.wallet.network-routing.view
quo.components.wallet.progress-bar.view
quo.components.wallet.summary-info.view
quo.components.wallet.token-input.view
@ -373,6 +374,7 @@
(def keypair quo.components.wallet.keypair.view/view)
(def network-amount quo.components.wallet.network-amount.view/view)
(def network-bridge quo.components.wallet.network-bridge.view/view)
(def network-routing quo.components.wallet.network-routing.view/view)
(def progress-bar quo.components.wallet.progress-bar.view/view)
(def summary-info quo.components.wallet.summary-info.view/view)
(def network-link quo.components.wallet.network-link.view/view)

View File

@ -0,0 +1,28 @@
(ns quo2.components.wallet.network-routing.component-spec
(:require [oops.core :as oops]
[quo2.components.wallet.network-routing.view :as network-routing]
[reagent.core :as reagent]
[test-helpers.component :as h]))
(h/describe "Network-routing tests"
(let [network {:amount 250 :max-amount 300 :network-name :unknown}
default-props {:networks [network network network]
:total-amount 500
:requesting-data? false
:on-amount-selected (fn [_new-amount _network-idx] nil)}]
(h/test "Renders Default"
(h/render [network-routing/view default-props])
(h/is-truthy (h/get-by-label-text :network-routing)))
(h/test "Renders bars inside"
(let [component (h/render [network-routing/view default-props])
rerender-fn #((oops/oget component "rerender") (reagent/as-element %))
component (h/get-by-label-text :network-routing)]
;; Fires on-layout callback since the total width is required
(h/fire-event :layout component #js {:nativeEvent #js {:layout #js {:width 1000}}})
;; Update props to trigger rerender, otherwise it won't be updated
(rerender-fn [network-routing/view (assoc default-props :requesting-data? true)])
;; Check number of networks rendered
(->> (js->clj (h/query-all-by-label-text :network-routing-bar))
(count)
(h/is-equal 3))))))

View File

@ -166,6 +166,7 @@
[status-im2.contexts.quo-preview.wallet.network-amount :as network-amount]
[status-im2.contexts.quo-preview.wallet.network-bridge :as network-bridge]
[status-im2.contexts.quo-preview.wallet.network-link :as network-link]
[status-im2.contexts.quo-preview.wallet.network-routing :as network-routing]
[status-im2.contexts.quo-preview.wallet.progress-bar :as progress-bar]
[status-im2.contexts.quo-preview.wallet.summary-info :as summary-info]
[status-im2.contexts.quo-preview.wallet.token-input :as token-input]
@ -447,6 +448,7 @@
{:name :network-amount :component network-amount/preview}
{:name :network-bridge :component network-bridge/preview}
{:name :network-link :component network-link/preview}
{:name :network-routing :component network-routing/preview}
{:name :progress-bar :component progress-bar/preview}
{:name :summary-info :component summary-info/preview}
{:name :token-input :component token-input/preview}

View File

@ -0,0 +1,94 @@
(ns status-im2.contexts.quo-preview.wallet.network-routing
(:require [quo.core :as quo]
[quo.foundations.colors :as colors]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im2.contexts.quo-preview.preview :as preview]))
(def descriptor
[{:label "Number of networks"
:key :number-networks
:type :select
:options [{:key 2} {:key 3} {:key 4} {:key 5}]}])
(defn- fake-call-to-get-amounts
[{:keys [new-amount fixed-index current-values on-success]}]
(let [number-networks (count current-values)
amount-difference (- (get current-values fixed-index) new-amount)
difference-distributed (/ amount-difference (dec number-networks))
new-values (assoc (mapv #(+ % difference-distributed) current-values)
fixed-index
new-amount)]
(js/setTimeout #(on-success new-values) (rand-nth (range 700 5000 250)))))
(defn preview-internal
[{:keys [total-amount number-networks] :as descriptor-state}]
(let [initial-amount (/ total-amount number-networks)
networks (reagent/atom
[{:amount initial-amount
:max-amount (descriptor-state :max-amount-0)
:network-name :ethereum}
{:amount initial-amount
:max-amount (descriptor-state :max-amount-1)
:network-name :arbitrum}
{:amount initial-amount
:max-amount (descriptor-state :max-amount-2)
:network-name :xDai}
{:amount initial-amount
:max-amount (descriptor-state :max-amount-3)
:network-name :optimism}
{:amount initial-amount
:max-amount (descriptor-state :max-amount-4)
:network-name :polygon}])
requesting-data? (reagent/atom false)]
(fn [_]
(let [asked-networks (vec (take number-networks @networks))
on-success-fn (fn [new-network-amounts]
(reset! requesting-data? false)
(swap! networks
#(map (fn [network new-amount]
(assoc network :amount new-amount))
%
new-network-amounts)))]
[rn/view
[quo/network-routing
{:total-amount total-amount
:networks asked-networks
:requesting-data? @requesting-data?
:on-amount-selected (fn [new-amount selected-idx]
(reset! requesting-data? true)
(fake-call-to-get-amounts
{:new-amount new-amount
:fixed-index selected-idx
:current-values (mapv :amount asked-networks)
:on-success on-success-fn}))}]
(reduce (fn [acc {:keys [amount max-amount network-name]}]
(conj acc
[rn/view
{:style {:flex-direction :row
:margin-vertical 12}}
[rn/view
{:style {:background-color (colors/custom-color network-name)
:width 24
:height 24
:margin-right 12}}]
[quo/text
"Max limit: " max-amount " Amount: " (subs (str amount) 0 6)]]))
[rn/view {:style {:margin-vertical 12}}
[quo/text "Total amount: " (reduce + (map :amount asked-networks))]]
asked-networks)]))))
(defn preview
[]
(let [descriptor-state (reagent/atom {:total-amount 400
:number-networks 4
:max-amount-0 350
:max-amount-1 350
:max-amount-2 300
:max-amount-3 250
:max-amount-4 200})]
(fn []
[preview/preview-container {:state descriptor-state :descriptor descriptor}
[rn/view {:style {:flex 1 :margin-vertical 28}}
^{:key (str "preview-network-routing-" (:number-networks @descriptor-state))}
[preview-internal @descriptor-state]]])))