chore(wallet): smooth collectible list loading transition

This commit is contained in:
Jamie Caprani 2024-05-24 22:44:16 +02:00 committed by GitHub
parent f17484f61b
commit 777b2bb8da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 248 additions and 154 deletions

View File

@ -10,12 +10,13 @@
:image - collection image :image - collection image
:theme - keyword -> :light/:dark" :theme - keyword -> :light/:dark"
[{:keys [image size on-load-end on-error] :or {size :size-24}}] [{:keys [image size on-load-start on-load-end on-error] :or {size :size-24}}]
(let [theme (quo.theme/use-theme)] (let [theme (quo.theme/use-theme)]
[rn/view {:style (style/collection-avatar-container theme size)} [rn/view {:style (style/collection-avatar-container theme size)}
[fast-image/fast-image [fast-image/fast-image
{:accessibility-label :collection-avatar {:accessibility-label :collection-avatar
:source image :source image
:on-load-start on-load-start
:on-load-end on-load-end :on-load-end on-load-end
:on-error on-error :on-error on-error
:style (style/collection-avatar size)}]])) :style (style/collection-avatar size)}]]))

View File

@ -5,18 +5,22 @@
(def container-border-radius 12) (def container-border-radius 12)
(def card-image-padding-vertical 3) (def card-image-padding-vertical 3)
(def card-image-padding-horizontal 3) (def card-image-padding-horizontal 3)
(def default-opacity-for-loader 1)
(def default-opacity-for-image 0)
(defn fallback (defn fallback
[{:keys [theme]}] [{:keys [theme opacity]}]
{:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme) [{:opacity opacity}
:border-style :dashed {:opacity default-opacity-for-image
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) :background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)
:border-width 1 :border-style :dashed
:border-radius container-border-radius :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:width "100%" :border-width 1
:aspect-ratio 1 :border-radius container-border-radius
:align-items :center :width "100%"
:justify-content :center}) :aspect-ratio 1
:align-items :center
:justify-content :center}])
(def collectible-counter (def collectible-counter
{:position :absolute {:position :absolute
@ -59,6 +63,15 @@
(def card-detail-text (def card-detail-text
{:flex 1}) {:flex 1})
(defn card-loader
[opacity]
[{:opacity opacity}
{:position :absolute
:opacity default-opacity-for-loader
:left 8
:align-items :center
:flex-direction :row}])
(def image-view-container (def image-view-container
{:aspect-ratio 1 {:aspect-ratio 1
:border-radius container-border-radius}) :border-radius container-border-radius})
@ -83,6 +96,7 @@
(defn loading-image (defn loading-image
[theme] [theme]
{:position :absolute {:position :absolute
:opacity default-opacity-for-loader
:top 0 :top 0
:bottom 0 :bottom 0
:left 0 :left 0
@ -91,8 +105,20 @@
:background-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme) :background-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme)
:z-index 2}) :z-index 2})
(defn loading-image-with-opacity
[theme opacity]
[{:opacity opacity}
(loading-image theme)])
(defn avatar-container (defn avatar-container
[loaded?] [opacity]
{:flex 1 [{:opacity opacity}
:flex-direction :row {:flex 1
:opacity (when-not loaded? 0)}) :flex-direction :row
:opacity default-opacity-for-image}])
(defn supported-file
[opacity]
[{:opacity opacity}
{:aspect-ratio 1
:opacity default-opacity-for-image}])

View File

@ -10,13 +10,54 @@
[quo.foundations.gradients :as gradients] [quo.foundations.gradients :as gradients]
[quo.theme] [quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.reanimated :as reanimated]
[schema.core :as schema] [schema.core :as schema]
[utils.datetime :as datetime]
[utils.i18n :as i18n])) [utils.i18n :as i18n]))
(def timing-options-out 650)
(def timing-options-in 1000)
(def first-load-time 500)
(def cached-load-time 200)
(def error-wait-time 800)
(defn on-load-end
[{:keys [load-time set-state loader-opacity image-opacity]}]
(reanimated/animate loader-opacity 0 timing-options-out)
(reanimated/animate image-opacity 1 timing-options-in)
(if (> load-time cached-load-time)
(js/setTimeout
(fn []
(set-state (fn [prev-state]
(assoc prev-state :image-loaded? true))))
first-load-time)
(set-state (fn [prev-state]
(assoc prev-state :image-loaded? true)))))
(defn on-load-error
[set-state]
(js/setTimeout (fn []
(set-state (fn [prev-state] (assoc prev-state :image-error? true))))
error-wait-time))
(defn on-load-avatar
[{:keys [load-time set-state loader-opacity avatar-opacity]}]
(reanimated/animate loader-opacity 0 timing-options-out)
(reanimated/animate avatar-opacity 1 timing-options-in)
(if (> load-time cached-load-time)
(js/setTimeout
(fn []
(set-state (fn [prev-state]
(assoc prev-state :avatar-loaded? true))))
first-load-time)
(set-state (fn [prev-state]
(assoc prev-state :avatar-loaded? true)))))
(defn- fallback-view (defn- fallback-view
[{:keys [label theme]}] [{:keys [label theme image-opacity]}]
[rn/view [reanimated/view
{:style (style/fallback {:theme theme})} {:style (style/fallback {:opacity image-opacity
:theme theme})}
[rn/view [rn/view
[icon/icon :i/sad {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}]] [icon/icon :i/sad {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}]]
[rn/view {:style {:height 4}}] [rn/view {:style {:height 4}}]
@ -34,77 +75,85 @@
[rn/view {:style (style/loading-message theme)}]) [rn/view {:style (style/loading-message theme)}])
(defn- loading-image (defn- loading-image
[{:keys [theme gradient-color-index]}] [{:keys [theme gradient-color-index loader-opacity]}]
[gradients/view [reanimated/view
{:theme theme {:style (style/loading-image-with-opacity theme loader-opacity)}
:container-style (style/loading-image theme) [gradients/view
:color-index gradient-color-index}]) {:theme theme
:container-style (style/loading-image theme)
:color-index gradient-color-index}]])
(defn- card-details (defn- card-details
[{:keys [community? avatar-image-src collectible-name theme state set-state]}] [{:keys [community? avatar-image-src collectible-name theme state set-state]}]
[rn/view {:style style/card-details-container} (let [loader-opacity (reanimated/use-shared-value 1)
(cond (not (:avatar-loaded? state)) avatar-opacity (reanimated/use-shared-value 0)
[rn/view {:style {:flex-direction :row}} [load-time set-load-time] (rn/use-state (datetime/now))]
[loading-square theme] [rn/view {:style style/card-details-container}
[loading-message theme]] [reanimated/view {:style (style/avatar-container avatar-opacity)}
(if community?
community? [preview-list/view
[:<> {:type :communities
[preview-list/view :size :size-20}
{:type :communities [avatar-image-src]]
:size :size-20} [collection-avatar/view
[avatar-image-src]] {:size :size-20
[rn/view {:style {:width 8}}] :on-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
[text/text :on-load-end #(on-load-avatar {:set-state set-state
{:size :paragraph-1 :load-time load-time
:weight :semi-bold :loader-opacity loader-opacity
:style style/card-detail-text} :avatar-opacity avatar-opacity})
collectible-name]]) :image avatar-image-src}])
[rn/view {:style {:width 8}}]
[rn/view [text/text
{:style (style/avatar-container (:avatar-loaded? state))} {:size :paragraph-1
[:<> :weight :semi-bold
[collection-avatar/view :ellipsize-mode :tail
{:size :size-20 :number-of-lines 1
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :avatar-loaded? true))) :style style/card-detail-text}
:image avatar-image-src}] collectible-name]]
[rn/view {:style {:width 8}}]] (when (not (:avatar-loaded? state))
[text/text [reanimated/view {:style (style/card-loader loader-opacity)}
{:size :paragraph-1 [loading-square theme]
:weight :semi-bold [loading-message theme]])]))
:ellipsize-mode :tail
:number-of-lines 1
:style style/card-detail-text}
collectible-name]]])
(defn- card-view (defn- card-view
[{:keys [avatar-image-src collectible-name community? counter state set-state [{:keys [avatar-image-src collectible-name community? counter state set-state
gradient-color-index image-src supported-file?]}] gradient-color-index image-src supported-file?]}]
(let [theme (quo.theme/use-theme)] (let [theme (quo.theme/use-theme)
loader-opacity (reanimated/use-shared-value (if supported-file? 1 0))
image-opacity (reanimated/use-shared-value (if supported-file? 0 1))
[load-time set-load-time] (rn/use-state (datetime/now))]
[rn/view {:style (style/card-view-container theme)} [rn/view {:style (style/card-view-container theme)}
[rn/view {:style {:aspect-ratio 1}} [rn/view {:style {:aspect-ratio 1}}
(cond (cond
(:image-error? state) (:image-error? state)
[fallback-view [fallback-view
{:theme theme {:image-opacity image-opacity
:label (i18n/label :t/cant-fetch-info)}] :theme theme
:label (i18n/label :t/cant-fetch-info)}]
(not supported-file?) (not supported-file?)
[fallback-view [fallback-view
{:theme theme {:image-opacity image-opacity
:label (i18n/label :t/unsupported-file)}] :theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state)) (not (:image-loaded? state))
[loading-image [loading-image
{:theme theme {:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}]) :gradient-color-index gradient-color-index}])
(when supported-file? (when supported-file?
[rn/view {:style {:aspect-ratio 1}} [reanimated/view {:style (style/supported-file image-opacity)}
[rn/image [rn/image
{:style style/image {:style style/image
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :image-loaded? true))) :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-error #(set-state (fn [prev-state] (assoc prev-state :image-error? true))) :on-load-end #(on-load-end {:load-time load-time
:source image-src}]])] :set-state set-state
:loader-opacity loader-opacity
:image-opacity image-opacity})
:on-error #(on-load-error set-state)
:source image-src}]])]
(when (and (:image-loaded? state) (not (:image-error? state)) counter) (when (and (:image-loaded? state) (not (:image-error? state)) counter)
[collectible-counter/view [collectible-counter/view
{:container-style style/collectible-counter {:container-style style/collectible-counter
@ -121,30 +170,40 @@
(defn- image-view (defn- image-view
[{:keys [avatar-image-src community? counter state set-state [{:keys [avatar-image-src community? counter state set-state
gradient-color-index image-src supported-file?]}] gradient-color-index image-src supported-file?]}]
(let [theme (quo.theme/use-theme)] (let [theme (quo.theme/use-theme)
loader-opacity (reanimated/use-shared-value (if supported-file? 1 0))
image-opacity (reanimated/use-shared-value (if supported-file? 0 1))
[load-time set-load-time] (rn/use-state (datetime/now))]
[rn/view {:style style/image-view-container} [rn/view {:style style/image-view-container}
(cond (cond
(:image-error? state) (:image-error? state)
[fallback-view [fallback-view
{:theme theme {:image-opacity image-opacity
:label (i18n/label :t/cant-fetch-info)}] :theme theme
:label (i18n/label :t/cant-fetch-info)}]
(not supported-file?) (not supported-file?)
[fallback-view [fallback-view
{:theme theme {:image-opacity image-opacity
:label (i18n/label :t/unsupported-file)}] :theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state)) (not (:image-loaded? state))
[loading-image [loading-image
{:theme theme {:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}]) :gradient-color-index gradient-color-index}])
(when supported-file? (when supported-file?
[rn/view {:style {:aspect-ratio 1}} [reanimated/view {:style (style/supported-file image-opacity)}
[rn/image [rn/image
{:style style/image {:style style/image
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :image-loaded? true))) :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-error #(set-state (fn [prev-state] (assoc prev-state :image-error? true))) :on-load-end #(on-load-end {:load-time load-time
:source image-src}]]) :set-state set-state
:loader-opacity loader-opacity
:image-opacity image-opacity})
:on-error #(on-load-error set-state)
:source image-src}]])
(when (and (:image-loaded? state) (not (:image-error? state)) counter) (when (and (:image-loaded? state) (not (:image-error? state)) counter)
[collectible-counter/view [collectible-counter/view
{:container-style style/collectible-counter {:container-style style/collectible-counter

View File

@ -105,23 +105,6 @@
(def fast-create-community-enabled? (def fast-create-community-enabled?
(enabled? (get-config :FAST_CREATE_COMMUNITY_ENABLED "0"))) (enabled? (get-config :FAST_CREATE_COMMUNITY_ENABLED "0")))
(def default-multiaccount
{:preview-privacy? blank-preview?
:wallet-legacy/visible-tokens {:mainnet #{:SNT}}
:currency :usd
:appearance 0
:profile-pictures-show-to 2
:profile-pictures-visibility 2
:log-level log-level
:webview-allow-permission-requests? false
:opensea-enabled? false
:link-previews-enabled-sites #{}
:link-preview-request-enabled true})
(defn default-visible-tokens
[chain]
(get-in default-multiaccount [:wallet-legacy/visible-tokens chain]))
(def waku-nodes-config (def waku-nodes-config
{:status.prod {:status.prod
["enrtree://AL65EKLJAUXKKPG43HVTML5EFFWEZ7L4LOKTLZCLJASG4DSESQZEC@prod.status.nodes.status.im"] ["enrtree://AL65EKLJAUXKKPG43HVTML5EFFWEZ7L4LOKTLZCLJASG4DSESQZEC@prod.status.nodes.status.im"]

View File

@ -11,6 +11,22 @@
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn- on-collectible-press
[{:keys [id]}]
(rf/dispatch [:wallet/get-collectible-details id]))
(defn- on-collectible-long-press
[{:keys [preview-url collectible-details]}]
(rf/dispatch [:show-bottom-sheet
{:content (fn []
[options-drawer/view
{:name (:name collectible-details)
:image (:uri preview-url)}])}]))
(defn- on-end-reached
[]
(rf/dispatch [:wallet/request-collectibles-for-current-viewing-account]))
(defn view (defn view
[{:keys [selected-tab]}] [{:keys [selected-tab]}]
(let [collectible-list (rf/sub (let [collectible-list (rf/sub
@ -20,19 +36,11 @@
(case selected-tab (case selected-tab
:assets [assets/view] :assets [assets/view]
:collectibles [collectibles/view :collectibles [collectibles/view
{:collectibles collectible-list {:collectibles collectible-list
:current-account-address current-account-address :current-account-address current-account-address
:on-end-reached #(rf/dispatch :on-end-reached on-end-reached
[:wallet/request-collectibles-for-current-viewing-account]) :on-collectible-press on-collectible-press
:on-collectible-press (fn [{:keys [id]}] :on-collectible-long-press on-collectible-long-press}]
(rf/dispatch [:wallet/get-collectible-details id]))
:on-collectible-long-press (fn [{:keys [preview-url collectible-details]}]
(rf/dispatch
[:show-bottom-sheet
{:content (fn []
[options-drawer/view
{:name (:name collectible-details)
:image (:uri preview-url)}])}]))}]
:activity [activity/view] :activity [activity/view]
:permissions [empty-tab/view :permissions [empty-tab/view
{:title (i18n/label :t/no-permissions) {:title (i18n/label :t/no-permissions)

View File

@ -1,7 +1,8 @@
(ns status-im.contexts.wallet.collectible.utils (ns status-im.contexts.wallet.collectible.utils
(:require [status-im.config :as config] (:require [status-im.config :as config]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.common.utils.networks :as network-utils])) [status-im.contexts.wallet.common.utils.networks :as network-utils]
[taoensso.timbre :as log]))
(defn collectible-balance (defn collectible-balance
[collectible] [collectible]
@ -23,7 +24,7 @@
(if (supported-collectible-types collectible-type) (if (supported-collectible-types collectible-type)
true true
(do (do
(println "unsupoorted collectible file type" collectible-type) (log/debug "unsupported collectible file type:" (or collectible-type "Unknown type"))
false))) false)))
(defn total-owned-collectible (defn total-owned-collectible

View File

@ -4,3 +4,7 @@
(def list-container-style (def list-container-style
{:margin-horizontal 12 {:margin-horizontal 12
:padding-bottom constants/floating-shell-button-height}) :padding-bottom constants/floating-shell-button-height})
(def collectible-container
{:padding 8
:flex 0.5})

View File

@ -9,10 +9,14 @@
[status-im.contexts.wallet.common.empty-tab.view :as empty-tab] [status-im.contexts.wallet.common.empty-tab.view :as empty-tab]
[utils.i18n :as i18n])) [utils.i18n :as i18n]))
(defn- render-fn (defn- collectible-item
[{:keys [preview-url collection-data ownership collectible-data] :as collectible} index address [{:keys [preview-url collection-data collectible-data total-owned on-press on-long-press]
on-press on-long-press] :as collectible}
(let [total-owned (utils/total-owned-collectible ownership address)] index]
(let [on-press-fn (rn/use-callback #(when on-press
(on-press collectible)))
on-long-press-fn (rn/use-callback #(when on-long-press
(on-long-press collectible)))]
[quo/collectible-list-item [quo/collectible-list-item
{:type :card {:type :card
:image-src (:uri preview-url) :image-src (:uri preview-url)
@ -21,18 +25,15 @@
:supported-file? (utils/supported-file? (:animation-media-type collectible-data)) :supported-file? (utils/supported-file? (:animation-media-type collectible-data))
:gradient-color-index (keyword (str "gradient-" (inc (mod index 5)))) :gradient-color-index (keyword (str "gradient-" (inc (mod index 5))))
:counter (utils/collectible-owned-counter total-owned) :counter (utils/collectible-owned-counter total-owned)
:container-style {:padding 8 :container-style style/collectible-container
:width "50%"} :on-press on-press-fn
:on-press #(when on-press :on-long-press on-long-press-fn}]))
(on-press collectible))
:on-long-press #(when on-long-press
(on-long-press collectible))}]))
(defn view (defn view
[{:keys [collectibles filtered? on-collectible-press on-end-reached current-account-address [{:keys [collectibles filtered? on-end-reached
on-collectible-long-press]}] on-collectible-press current-account-address on-collectible-long-press]}]
(let [no-results-match-query? (and filtered? (empty? collectibles)) (let [theme (quo.theme/use-theme)
theme (quo.theme/use-theme)] no-results-match-query? (and filtered? (empty? collectibles))]
(cond (cond
no-results-match-query? no-results-match-query?
[rn/view {:style {:flex 1 :justify-content :center}} [rn/view {:style {:flex 1 :justify-content :center}}
@ -48,17 +49,25 @@
:image (resources/get-themed-image :no-collectibles theme)}] :image (resources/get-themed-image :no-collectibles theme)}]
:else :else
[rn/flat-list ;; TODO: https://github.com/status-im/status-mobile/issues/20137
{:data collectibles ;; 1. If possible, move `collectibles-data` calculation to a subscription
:style {:flex 1} ;; 2. Optimization: do not recalculate all the collectibles, process only the new ones
:content-container-style style/list-container-style (let [collectibles-data (map-indexed (fn [index {:keys [ownership] :as collectible}]
:window-size 11 (assoc collectible
:num-columns 2 :total-owned (utils/total-owned-collectible
:render-fn (fn [item index] ownership
(render-fn item current-account-address)
index :on-long-press on-collectible-long-press
current-account-address :on-press on-collectible-press
on-collectible-press :collectible-index index))
on-collectible-long-press)) collectibles)]
:on-end-reached on-end-reached [rn/flat-list
:on-end-reached-threshold 4}]))) {:data collectibles-data
:style {:flex 1}
:content-container-style style/list-container-style
:window-size 11
:num-columns 2
:render-fn collectible-item
:on-end-reached on-end-reached
:key-fn :collectible-index
:on-end-reached-threshold 4}]))))

View File

@ -8,6 +8,22 @@
[status-im.contexts.wallet.home.tabs.style :as style] [status-im.contexts.wallet.home.tabs.style :as style]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn- on-collectible-long-press
[{:keys [preview-url collectible-details id]}]
(let [chain-id (get-in id [:contract-id :chain-id])
address (get-in id [:contract-id :address])]
(rf/dispatch [:show-bottom-sheet
{:content (fn []
[options-drawer/view
{:chain-id chain-id
:address address
:name (:name collectible-details)
:image (:uri preview-url)}])}])))
(defn- on-collectible-press
[{:keys [id]}]
(rf/dispatch [:wallet/get-collectible-details id]))
(defn view (defn view
[{:keys [selected-tab]}] [{:keys [selected-tab]}]
(let [collectible-list (rf/sub [:wallet/all-collectibles-list-in-selected-networks]) (let [collectible-list (rf/sub [:wallet/all-collectibles-list-in-selected-networks])
@ -18,20 +34,7 @@
:assets [assets/view] :assets [assets/view]
:collectibles [collectibles/view :collectibles [collectibles/view
{:collectibles collectible-list {:collectibles collectible-list
:on-collectible-long-press (fn [{:keys [preview-url collectible-details id]}] :on-collectible-long-press on-collectible-long-press
(let [chain-id (get-in id [:contract-id :chain-id])
address (get-in id [:contract-id :address])]
(rf/dispatch
[:show-bottom-sheet
{:content (fn []
[options-drawer/view
{:chain-id chain-id
:address address
:name (:name
collectible-details)
:image (:uri
preview-url)}])}])))
:on-end-reached request-collectibles :on-end-reached request-collectibles
:on-collectible-press (fn [{:keys [id]}] :on-collectible-press on-collectible-press}]
(rf/dispatch [:wallet/get-collectible-details id]))}]
[activity/view {:activities []}])])) [activity/view {:activities []}])]))