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
: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)]
[rn/view {:style (style/collection-avatar-container theme size)}
[fast-image/fast-image
{:accessibility-label :collection-avatar
:source image
:on-load-start on-load-start
:on-load-end on-load-end
:on-error on-error
:style (style/collection-avatar size)}]]))

View File

@ -5,18 +5,22 @@
(def container-border-radius 12)
(def card-image-padding-vertical 3)
(def card-image-padding-horizontal 3)
(def default-opacity-for-loader 1)
(def default-opacity-for-image 0)
(defn fallback
[{:keys [theme]}]
{:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)
:border-style :dashed
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:border-width 1
:border-radius container-border-radius
:width "100%"
:aspect-ratio 1
:align-items :center
:justify-content :center})
[{:keys [theme opacity]}]
[{:opacity opacity}
{:opacity default-opacity-for-image
:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)
:border-style :dashed
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
:border-width 1
:border-radius container-border-radius
:width "100%"
:aspect-ratio 1
:align-items :center
:justify-content :center}])
(def collectible-counter
{:position :absolute
@ -59,6 +63,15 @@
(def card-detail-text
{: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
{:aspect-ratio 1
:border-radius container-border-radius})
@ -83,6 +96,7 @@
(defn loading-image
[theme]
{:position :absolute
:opacity default-opacity-for-loader
:top 0
:bottom 0
:left 0
@ -91,8 +105,20 @@
:background-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme)
:z-index 2})
(defn loading-image-with-opacity
[theme opacity]
[{:opacity opacity}
(loading-image theme)])
(defn avatar-container
[loaded?]
{:flex 1
:flex-direction :row
:opacity (when-not loaded? 0)})
[opacity]
[{:opacity opacity}
{:flex 1
: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.theme]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[schema.core :as schema]
[utils.datetime :as datetime]
[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
[{:keys [label theme]}]
[rn/view
{:style (style/fallback {:theme theme})}
[{:keys [label theme image-opacity]}]
[reanimated/view
{:style (style/fallback {:opacity image-opacity
:theme theme})}
[rn/view
[icon/icon :i/sad {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}]]
[rn/view {:style {:height 4}}]
@ -34,77 +75,85 @@
[rn/view {:style (style/loading-message theme)}])
(defn- loading-image
[{:keys [theme gradient-color-index]}]
[gradients/view
{:theme theme
:container-style (style/loading-image theme)
:color-index gradient-color-index}])
[{:keys [theme gradient-color-index loader-opacity]}]
[reanimated/view
{:style (style/loading-image-with-opacity theme loader-opacity)}
[gradients/view
{:theme theme
:container-style (style/loading-image theme)
:color-index gradient-color-index}]])
(defn- card-details
[{:keys [community? avatar-image-src collectible-name theme state set-state]}]
[rn/view {:style style/card-details-container}
(cond (not (:avatar-loaded? state))
[rn/view {:style {:flex-direction :row}}
[loading-square theme]
[loading-message theme]]
community?
[:<>
[preview-list/view
{:type :communities
:size :size-20}
[avatar-image-src]]
[rn/view {:style {:width 8}}]
[text/text
{:size :paragraph-1
:weight :semi-bold
:style style/card-detail-text}
collectible-name]])
[rn/view
{:style (style/avatar-container (:avatar-loaded? state))}
[:<>
[collection-avatar/view
{:size :size-20
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :avatar-loaded? true)))
:image avatar-image-src}]
[rn/view {:style {:width 8}}]]
[text/text
{:size :paragraph-1
:weight :semi-bold
:ellipsize-mode :tail
:number-of-lines 1
:style style/card-detail-text}
collectible-name]]])
(let [loader-opacity (reanimated/use-shared-value 1)
avatar-opacity (reanimated/use-shared-value 0)
[load-time set-load-time] (rn/use-state (datetime/now))]
[rn/view {:style style/card-details-container}
[reanimated/view {:style (style/avatar-container avatar-opacity)}
(if community?
[preview-list/view
{:type :communities
:size :size-20}
[avatar-image-src]]
[collection-avatar/view
{:size :size-20
:on-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-avatar {:set-state set-state
:load-time load-time
:loader-opacity loader-opacity
:avatar-opacity avatar-opacity})
:image avatar-image-src}])
[rn/view {:style {:width 8}}]
[text/text
{:size :paragraph-1
:weight :semi-bold
:ellipsize-mode :tail
:number-of-lines 1
:style style/card-detail-text}
collectible-name]]
(when (not (:avatar-loaded? state))
[reanimated/view {:style (style/card-loader loader-opacity)}
[loading-square theme]
[loading-message theme]])]))
(defn- card-view
[{:keys [avatar-image-src collectible-name community? counter state set-state
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 {:aspect-ratio 1}}
(cond
(:image-error? state)
[fallback-view
{:theme theme
:label (i18n/label :t/cant-fetch-info)}]
{:image-opacity image-opacity
:theme theme
:label (i18n/label :t/cant-fetch-info)}]
(not supported-file?)
[fallback-view
{:theme theme
:label (i18n/label :t/unsupported-file)}]
{:image-opacity image-opacity
:theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state))
[loading-image
{:theme theme
{:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}])
(when supported-file?
[rn/view {:style {:aspect-ratio 1}}
[reanimated/view {:style (style/supported-file image-opacity)}
[rn/image
{:style style/image
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :image-loaded? true)))
:on-error #(set-state (fn [prev-state] (assoc prev-state :image-error? true)))
:source image-src}]])]
{:style style/image
:on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-end {:load-time load-time
: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)
[collectible-counter/view
{:container-style style/collectible-counter
@ -121,30 +170,40 @@
(defn- image-view
[{:keys [avatar-image-src community? counter state set-state
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}
(cond
(:image-error? state)
[fallback-view
{:theme theme
:label (i18n/label :t/cant-fetch-info)}]
{:image-opacity image-opacity
:theme theme
:label (i18n/label :t/cant-fetch-info)}]
(not supported-file?)
[fallback-view
{:theme theme
:label (i18n/label :t/unsupported-file)}]
{:image-opacity image-opacity
:theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state))
[loading-image
{:theme theme
{:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}])
(when supported-file?
[rn/view {:style {:aspect-ratio 1}}
[reanimated/view {:style (style/supported-file image-opacity)}
[rn/image
{:style style/image
:on-load-end #(set-state (fn [prev-state] (assoc prev-state :image-loaded? true)))
:on-error #(set-state (fn [prev-state] (assoc prev-state :image-error? true)))
:source image-src}]])
{:style style/image
:on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-end {:load-time load-time
: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)
[collectible-counter/view
{:container-style style/collectible-counter

View File

@ -105,23 +105,6 @@
(def fast-create-community-enabled?
(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
{:status.prod
["enrtree://AL65EKLJAUXKKPG43HVTML5EFFWEZ7L4LOKTLZCLJASG4DSESQZEC@prod.status.nodes.status.im"]

View File

@ -11,6 +11,22 @@
[utils.i18n :as i18n]
[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
[{:keys [selected-tab]}]
(let [collectible-list (rf/sub
@ -20,19 +36,11 @@
(case selected-tab
:assets [assets/view]
:collectibles [collectibles/view
{:collectibles collectible-list
:current-account-address current-account-address
:on-end-reached #(rf/dispatch
[:wallet/request-collectibles-for-current-viewing-account])
:on-collectible-press (fn [{:keys [id]}]
(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)}])}]))}]
{:collectibles collectible-list
:current-account-address current-account-address
:on-end-reached on-end-reached
:on-collectible-press on-collectible-press
:on-collectible-long-press on-collectible-long-press}]
:activity [activity/view]
:permissions [empty-tab/view
{:title (i18n/label :t/no-permissions)

View File

@ -1,7 +1,8 @@
(ns status-im.contexts.wallet.collectible.utils
(:require [status-im.config :as config]
[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
[collectible]
@ -23,7 +24,7 @@
(if (supported-collectible-types collectible-type)
true
(do
(println "unsupoorted collectible file type" collectible-type)
(log/debug "unsupported collectible file type:" (or collectible-type "Unknown type"))
false)))
(defn total-owned-collectible

View File

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