From 777b2bb8da4797d06b0f198bacbea2482058405d Mon Sep 17 00:00:00 2001 From: Jamie Caprani Date: Fri, 24 May 2024 22:44:16 +0200 Subject: [PATCH] chore(wallet): smooth collectible list loading transition --- .../avatars/collection_avatar/view.cljs | 3 +- .../profile/collectible_list_item/style.cljs | 54 +++-- .../profile/collectible_list_item/view.cljs | 187 ++++++++++++------ src/status_im/config.cljs | 17 -- .../contexts/wallet/account/tabs/view.cljs | 34 ++-- .../contexts/wallet/collectible/utils.cljs | 5 +- .../wallet/common/collectibles_tab/style.cljs | 4 + .../wallet/common/collectibles_tab/view.cljs | 65 +++--- .../contexts/wallet/home/tabs/view.cljs | 33 ++-- 9 files changed, 248 insertions(+), 154 deletions(-) diff --git a/src/quo/components/avatars/collection_avatar/view.cljs b/src/quo/components/avatars/collection_avatar/view.cljs index f5786ac04b..3afc03bd21 100644 --- a/src/quo/components/avatars/collection_avatar/view.cljs +++ b/src/quo/components/avatars/collection_avatar/view.cljs @@ -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)}]])) diff --git a/src/quo/components/profile/collectible_list_item/style.cljs b/src/quo/components/profile/collectible_list_item/style.cljs index 3b187a5b5a..b163c1008b 100644 --- a/src/quo/components/profile/collectible_list_item/style.cljs +++ b/src/quo/components/profile/collectible_list_item/style.cljs @@ -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}]) diff --git a/src/quo/components/profile/collectible_list_item/view.cljs b/src/quo/components/profile/collectible_list_item/view.cljs index b0e58ff6b0..5fcc274aa8 100644 --- a/src/quo/components/profile/collectible_list_item/view.cljs +++ b/src/quo/components/profile/collectible_list_item/view.cljs @@ -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 diff --git a/src/status_im/config.cljs b/src/status_im/config.cljs index 99823cb52c..fe89570e1b 100644 --- a/src/status_im/config.cljs +++ b/src/status_im/config.cljs @@ -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"] diff --git a/src/status_im/contexts/wallet/account/tabs/view.cljs b/src/status_im/contexts/wallet/account/tabs/view.cljs index fd06ce5cad..aabef2c98a 100644 --- a/src/status_im/contexts/wallet/account/tabs/view.cljs +++ b/src/status_im/contexts/wallet/account/tabs/view.cljs @@ -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) diff --git a/src/status_im/contexts/wallet/collectible/utils.cljs b/src/status_im/contexts/wallet/collectible/utils.cljs index 308648f06e..4e8d2afc41 100644 --- a/src/status_im/contexts/wallet/collectible/utils.cljs +++ b/src/status_im/contexts/wallet/collectible/utils.cljs @@ -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 diff --git a/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs b/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs index b2139048a9..05895ba12d 100644 --- a/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs +++ b/src/status_im/contexts/wallet/common/collectibles_tab/style.cljs @@ -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}) diff --git a/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs b/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs index b19ecf2666..9d09344884 100644 --- a/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs +++ b/src/status_im/contexts/wallet/common/collectibles_tab/view.cljs @@ -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}])))) diff --git a/src/status_im/contexts/wallet/home/tabs/view.cljs b/src/status_im/contexts/wallet/home/tabs/view.cljs index b02b92137c..1871381ea9 100644 --- a/src/status_im/contexts/wallet/home/tabs/view.cljs +++ b/src/status_im/contexts/wallet/home/tabs/view.cljs @@ -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 []}])]))