fix(wallet): Improve collectible UX and fix data displayed (#20657)

* Add variant unknown variant for collectibles

* Fix blank collectibles listed

* Improve `expanded-collectible` animation and add support for different gradient-colors

* Make :wallet/collectible-details-owner subscription depend on `wallet/accounts` instead of `:wallet`

* Make collectible tabs component more flexible

* Remove now unused subscriptions

* Improve UX navigation to collectible detail page

* Pass the current collectible gradient-color when a collectible is pressed

* Fix tests
This commit is contained in:
Ulises Manuel 2024-07-08 14:19:01 -06:00 committed by GitHub
parent 4c4a8b65d4
commit 7774c4eac1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 325 additions and 324 deletions

View File

@ -11,7 +11,7 @@
(defn fallback (defn fallback
[{:keys [theme opacity]}] [{:keys [theme opacity]}]
[{:opacity opacity} [{:opacity opacity}
{:opacity default-opacity-for-image {:opacity 1
:background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme) :background-color (colors/theme-colors colors/neutral-2_5 colors/neutral-90 theme)
:border-style :dashed :border-style :dashed
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)
@ -60,8 +60,13 @@
:padding-right 0 :padding-right 0
:padding-top 4}) :padding-top 4})
(def card-detail-text (defn card-detail-text
{:flex 1}) [empty-name? theme]
{:flex 1
:margin-right 8
:color (if empty-name?
(colors/theme-colors colors/neutral-50 colors/neutral-40 theme)
(colors/theme-colors colors/neutral-100 colors/white theme))})
(defn card-loader (defn card-loader
[opacity] [opacity]
@ -115,6 +120,7 @@
[{:opacity opacity} [{:opacity opacity}
{:flex 1 {:flex 1
:flex-direction :row :flex-direction :row
:column-gap 8
:opacity default-opacity-for-image}]) :opacity default-opacity-for-image}])
(defn supported-file (defn supported-file

View File

@ -1,5 +1,6 @@
(ns quo.components.profile.collectible-list-item.view (ns quo.components.profile.collectible-list-item.view
(:require (:require
[clojure.string :as string]
[quo.components.avatars.collection-avatar.view :as collection-avatar] [quo.components.avatars.collection-avatar.view :as collection-avatar]
[quo.components.counter.collectible-counter.view :as collectible-counter] [quo.components.counter.collectible-counter.view :as collectible-counter]
[quo.components.icon :as icon] [quo.components.icon :as icon]
@ -21,7 +22,7 @@
(def cached-load-time 200) (def cached-load-time 200)
(def error-wait-time 800) (def error-wait-time 800)
(defn on-load-end (defn animate-loading-image
[{:keys [load-time set-state loader-opacity image-opacity]}] [{:keys [load-time set-state loader-opacity image-opacity]}]
(reanimated/animate loader-opacity 0 timing-options-out) (reanimated/animate loader-opacity 0 timing-options-out)
(reanimated/animate image-opacity 1 timing-options-in) (reanimated/animate image-opacity 1 timing-options-in)
@ -60,12 +61,12 @@
(assoc prev-state :avatar-loaded? true))))) (assoc prev-state :avatar-loaded? true)))))
(defn- fallback-view (defn- fallback-view
[{:keys [label theme image-opacity]}] [{:keys [label theme image-opacity on-load-end]}]
[reanimated/view (rn/use-mount on-load-end)
{:style (style/fallback {:opacity image-opacity [reanimated/view {:style (style/fallback {:opacity image-opacity :theme theme})}
: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}}]
[text/text [text/text
{:size :paragraph-2 {:size :paragraph-2
@ -93,31 +94,43 @@
[{:keys [community? avatar-image-src collectible-name theme state set-state]}] [{:keys [community? avatar-image-src collectible-name theme state set-state]}]
(let [loader-opacity (reanimated/use-shared-value 1) (let [loader-opacity (reanimated/use-shared-value 1)
avatar-opacity (reanimated/use-shared-value 0) avatar-opacity (reanimated/use-shared-value 0)
[load-time set-load-time] (rn/use-state (datetime/now))] [load-time set-load-time] (rn/use-state (datetime/now))
set-avatar-loaded (fn []
(on-load-avatar {:set-state set-state
:load-time load-time
:loader-opacity loader-opacity
:avatar-opacity avatar-opacity}))
empty-name? (string/blank? collectible-name)]
(rn/use-mount (fn []
(when (string/blank? avatar-image-src)
(set-avatar-loaded))))
[rn/view {:style style/card-details-container} [rn/view {:style style/card-details-container}
[reanimated/view {:style (style/avatar-container avatar-opacity)} [reanimated/view {:style (style/avatar-container avatar-opacity)}
(if community? (cond
[preview-list/view (string/blank? avatar-image-src)
{:type :communities [loading-square theme]
:size :size-20}
community?
[preview-list/view {:type :communities :size :size-20}
[avatar-image-src]] [avatar-image-src]]
:else
[collection-avatar/view [collection-avatar/view
{:size :size-20 {:size :size-20
:on-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) :on-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-avatar {:set-state set-state :on-load-end set-avatar-loaded
:load-time load-time
:loader-opacity loader-opacity
:avatar-opacity avatar-opacity})
:image avatar-image-src}]) :image avatar-image-src}])
[rn/view {:style {:width 8}}]
[text/text [text/text
{:size :paragraph-1 {:size :paragraph-1
:weight :semi-bold :weight :semi-bold
:ellipsize-mode :tail :ellipsize-mode :tail
:number-of-lines 1 :number-of-lines 1
:style style/card-detail-text} :style (style/card-detail-text empty-name? theme)}
collectible-name]] (if empty-name?
(when (not (:avatar-loaded? state)) (i18n/label :t/unknown)
collectible-name)]]
(when-not (:avatar-loaded? state)
[reanimated/view {:style (style/card-loader loader-opacity)} [reanimated/view {:style (style/card-loader loader-opacity)}
[loading-square theme] [loading-square theme]
[loading-message theme]])])) [loading-message theme]])]))
@ -128,40 +141,44 @@
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
loader-opacity (reanimated/use-shared-value (if supported-file? 1 0)) loader-opacity (reanimated/use-shared-value (if supported-file? 1 0))
image-opacity (reanimated/use-shared-value (if supported-file? 0 1)) image-opacity (reanimated/use-shared-value (if supported-file? 0 1))
[load-time set-load-time] (rn/use-state (datetime/now))] [load-time set-load-time] (rn/use-state (datetime/now))
set-image-loaded (fn []
(animate-loading-image {:load-time load-time
:set-state set-state
:loader-opacity loader-opacity
:image-opacity image-opacity}))]
[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 {:image-opacity image-opacity
{:image-opacity image-opacity :on-load-end set-image-loaded
:theme theme :theme theme
:label (i18n/label :t/cant-fetch-info)}] :label (i18n/label :t/cant-fetch-info)}]
(not supported-file?) [fallback-view
{:image-opacity image-opacity
:on-load-end set-image-loaded
:theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state)) [loading-image
{:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}])
(not supported-file?)
[fallback-view
{:image-opacity image-opacity
:theme theme
:label (i18n/label :t/unsupported-file)}]
(not (:image-loaded? state))
[loading-image
{:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}])
(when supported-file? (when supported-file?
[reanimated/view {:style (style/supported-file image-opacity)} [reanimated/view {:style (style/supported-file image-opacity)}
[rn/image [rn/image
{:style style/image {:style style/image
:on-load #(on-load % set-state) :on-load #(on-load % set-state)
:on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-end {:load-time load-time :on-load-end set-image-loaded
:set-state set-state
:loader-opacity loader-opacity
:image-opacity image-opacity})
:on-error #(on-load-error set-state) :on-error #(on-load-error set-state)
:source image-src}]])] :source image-src}]])]
(when (and (:image-loaded? state) (not (:image-error? state)) counter)
(when (and (or (:image-loaded? state)
(not supported-file?))
(not (:image-error? state))
counter)
[collectible-counter/view [collectible-counter/view
{:container-style style/collectible-counter {:container-style style/collectible-counter
:size :size-24 :size :size-24
@ -206,10 +223,10 @@
{:style style/image {:style style/image
:on-load #(on-load % set-state) :on-load #(on-load % set-state)
:on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) :on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time)))
:on-load-end #(on-load-end {:load-time load-time :on-load-end #(animate-loading-image {:load-time load-time
:set-state set-state :set-state set-state
:loader-opacity loader-opacity :loader-opacity loader-opacity
:image-opacity image-opacity}) :image-opacity image-opacity})
:on-error #(on-load-error set-state) :on-error #(on-load-error set-state)
:source image-src}]]) :source image-src}]])
(when (and (:image-loaded? state) (not (:image-error? state)) counter) (when (and (:image-loaded? state) (not (:image-error? state)) counter)

View File

@ -11,38 +11,24 @@
[react-native.core :as rn] [react-native.core :as rn]
[react-native.reanimated :as reanimated] [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 loader-out 650)
(def timing-options-in 1000) (def image-in 1000)
(def first-load-time 500)
(def cached-load-time 200)
(def error-wait-time 800) (def error-wait-time 800)
(defn on-load-end (defn on-load-end
[{:keys [load-time set-state loader-opacity image-opacity]}] [{:keys [loader-opacity image-opacity]}]
(reanimated/animate loader-opacity 0 timing-options-out) (reanimated/animate loader-opacity 0 loader-out)
(reanimated/animate image-opacity 1 timing-options-in) (reanimated/animate image-opacity 1 image-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 (defn on-load-error
[set-state] [set-error]
(js/setTimeout (fn [] (js/setTimeout set-error error-wait-time))
(set-state (fn [prev-state] (assoc prev-state :image-error? true))))
error-wait-time))
(defn- loading-image (defn- loading-image
[{:keys [theme gradient-color-index loader-opacity aspect-ratio]}] [{:keys [theme gradient-color-index loader-opacity aspect-ratio]}]
[reanimated/view [reanimated/view {:style (style/loading-image-with-opacity loader-opacity)}
{:style (style/loading-image-with-opacity loader-opacity)}
[gradients/view [gradients/view
{:theme theme {:theme theme
:container-style (style/gradient-view aspect-ratio) :container-style (style/gradient-view aspect-ratio)
@ -67,62 +53,64 @@
:style {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}} :style {:color (colors/theme-colors colors/neutral-40 colors/neutral-50 theme)}}
label]]) label]])
(defn- collectible-image
[{:keys [aspect-ratio theme gradient-color-index supported-file? set-error native-ID
square? image-src on-collectible-load counter]}]
(let [loader-opacity (reanimated/use-shared-value (if supported-file? 1 0))
image-opacity (reanimated/use-shared-value (if supported-file? 0 1))]
[:<>
[loading-image
{:aspect-ratio aspect-ratio
:loader-opacity loader-opacity
:theme theme
:gradient-color-index gradient-color-index}]
[reanimated/view {:style (style/supported-file image-opacity)}
[rn/image
{:style (style/image square? aspect-ratio theme)
:source image-src
:native-ID native-ID
:on-load-end (fn []
(on-load-end {:loader-opacity loader-opacity
:image-opacity image-opacity}))
:on-error #(on-load-error set-error)
:on-load on-collectible-load}]
(when counter
[counter-view counter])
[rn/view {:style (style/collectible-border theme)}]]]))
(defn view-internal (defn view-internal
[{:keys [container-style square? on-press counter image-src native-ID supported-file? [{:keys [container-style square? on-press counter image-src native-ID supported-file?
on-collectible-load aspect-ratio]}] on-collectible-load aspect-ratio gradient-color-index]
(let [theme (quo.theme/use-theme) :or {gradient-color-index :gradient-1
loader-opacity (reanimated/use-shared-value on-collectible-load (fn [])}}]
(if supported-file? 1 0)) (let [theme (quo.theme/use-theme)
image-opacity (reanimated/use-shared-value [error? set-error] (rn/use-state (or (nil? image-src)
(if supported-file? 0 1)) (string/blank? image-src)))]
[load-time set-load-time] (rn/use-state (datetime/now))
[state set-state] (rn/use-state {:image-loaded? false
:image-error? (or (nil? image-src)
(string/blank?
image-src))})]
[rn/pressable [rn/pressable
{:on-press (when (and (not (:image-error? state)) supported-file?) on-press) {:style (merge container-style (style/container aspect-ratio))
:accessibility-label :expanded-collectible :accessibility-label :expanded-collectible
:style (merge container-style :on-press (when (and (not error?) supported-file?)
(style/container aspect-ratio))} on-press)}
(cond (if (or (not supported-file?) error?)
(not supported-file?)
[fallback-view [fallback-view
{:aspect-ratio aspect-ratio {:label (i18n/label (if-not supported-file?
:label (i18n/label :t/unsupported-file) :t/unsupported-file
:counter counter :t/cant-fetch-info))
:theme theme
:on-mount on-collectible-load}]
(:image-error? state)
[fallback-view
{:label (i18n/label :t/cant-fetch-info)
:counter counter :counter counter
:theme theme :theme theme
:on-mount on-collectible-load}] :on-mount on-collectible-load}]
[collectible-image
(not (:image-loaded? state))
[loading-image
{:aspect-ratio aspect-ratio {:aspect-ratio aspect-ratio
:loader-opacity loader-opacity
:theme theme :theme theme
:gradient-color-index :gradient-5}]) :supported-file? supported-file?
(when supported-file? :set-error set-error
[reanimated/view {:style (style/supported-file image-opacity)} :native-ID native-ID
[rn/image :square? square?
{:style (style/image square? aspect-ratio theme) :image-src image-src
:source image-src :on-collectible-load on-collectible-load
:native-ID native-ID :counter counter
:on-load-start #(set-load-time (fn [start-time] (- (datetime/now) start-time))) :gradient-color-index gradient-color-index}])]))
: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)
:on-load on-collectible-load}]
(when counter
[counter-view counter])
[rn/view {:style (style/collectible-border theme)}]])]))
(def ?schema (def ?schema
[:=> [:=>
@ -137,7 +125,8 @@
[:square? {:optional true} [:maybe boolean?]] [:square? {:optional true} [:maybe boolean?]]
[:counter {:optional true} [:maybe string?]] [:counter {:optional true} [:maybe string?]]
[:on-press {:optional true} [:maybe fn?]] [:on-press {:optional true} [:maybe fn?]]
[:on-collectible-load {:optional true} [:maybe fn?]]]]] [:on-collectible-load {:optional true} [:maybe fn?]]
[:gradient-color-index {:optional true} [:maybe keyword?]]]]]
:any]) :any])
(def view (schema/instrument #'view-internal ?schema)) (def view (schema/instrument #'view-internal ?schema))

View File

@ -11,9 +11,7 @@
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn- on-collectible-press (def on-collectible-press #(rf/dispatch [:wallet/navigate-to-collectible-details %]))
[{:keys [id]} aspect-ratio]
(rf/dispatch [:wallet/get-collectible-details id aspect-ratio]))
(defn- on-collectible-long-press (defn- on-collectible-long-press
[{:keys [preview-url collectible-details id]}] [{:keys [preview-url collectible-details id]}]

View File

@ -1,5 +1,6 @@
(ns status-im.contexts.wallet.collectible.events (ns status-im.contexts.wallet.collectible.events
(:require [camel-snake-kebab.extras :as cske] (:require [camel-snake-kebab.extras :as cske]
[clojure.string :as string]
[react-native.platform :as platform] [react-native.platform :as platform]
[status-im.contexts.wallet.collectible.utils :as collectible-utils] [status-im.contexts.wallet.collectible.utils :as collectible-utils]
[taoensso.timbre :as log] [taoensso.timbre :as log]
@ -44,13 +45,6 @@
(rf/reg-event-fx :wallet/clear-stored-collectibles clear-stored-collectibles) (rf/reg-event-fx :wallet/clear-stored-collectibles clear-stored-collectibles)
(defn store-last-collectible-details
[{:keys [db]} [collectible]]
{:db (assoc-in db [:wallet :last-collectible-details] collectible)
:dispatch [:navigate-to :screen/wallet.collectible]})
(rf/reg-event-fx :wallet/store-last-collectible-details store-last-collectible-details)
(rf/reg-event-fx (rf/reg-event-fx
:wallet/request-new-collectibles-for-account :wallet/request-new-collectibles-for-account
(fn [{:keys [db]} [{:keys [request-id account amount]}]] (fn [{:keys [db]} [{:keys [request-id account amount]}]]
@ -166,39 +160,58 @@
[:dispatch [:wallet/flush-collectibles-fetched]])]}))) [:dispatch [:wallet/flush-collectibles-fetched]])]})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet/get-collectible-details :wallet/navigate-to-collectible-details
(fn [{:keys [db]} [collectible-id aspect-ratio]] (fn [{:keys [db]}
[{{collectible-id :id :as collectible} :collectible
aspect-ratio :aspect-ratio
gradient-color :gradient-color}]]
(let [request-id 0 (let [request-id 0
collectible-id-converted (cske/transform-keys transforms/->PascalCaseKeyword collectible-id) collectible-id-converted (cske/transform-keys transforms/->PascalCaseKeyword collectible-id)
data-type (collectible-data-types :details) data-type (collectible-data-types :details)
request-params [request-id [collectible-id-converted] data-type]] request-params [request-id [collectible-id-converted] data-type]]
{:db (assoc-in db [:wallet :last-collectible-aspect-ratio] aspect-ratio) {:db (assoc-in db
[:wallet :ui :collectible]
{:details collectible
:aspect-ratio aspect-ratio
:gradient-color gradient-color})
:fx [[:json-rpc/call :fx [[:json-rpc/call
[{:method "wallet_getCollectiblesByUniqueIDAsync" [{:method "wallet_getCollectiblesByUniqueIDAsync"
:params request-params :params request-params
:on-error (fn [error] :on-error (fn [error]
(log/error "failed to request collectible" (log/error "failed to request collectible"
{:event :wallet/get-collectible-details {:event :wallet/navigate-to-collectible-details
:error error :error error
:params request-params}))}]]]}))) :params request-params}))}]]
;; We delay the navigation because we need re-frame to update the DB on time.
;; By doing it, we skip a blink while visiting the collectible detail page.
[:dispatch-later
{:ms 1
:dispatch [:navigate-to :screen/wallet.collectible]}]]})))
(defn- keep-not-empty-value
[old-value new-value]
(if (or (string/blank? new-value) (nil? new-value))
old-value
new-value))
(def merge-skipping-empty-values (partial merge-with keep-not-empty-value))
(rf/reg-event-fx (rf/reg-event-fx
:wallet/get-collectible-details-done :wallet/get-collectible-details-done
(fn [_ [{:keys [message]}]] (fn [{db :db} [{:keys [message]}]]
(let [response (cske/transform-keys transforms/->kebab-case-keyword (let [response (cske/transform-keys transforms/->kebab-case-keyword
(transforms/json->clj message)) (transforms/json->clj message))
{:keys [collectibles]} response {[collectible] :collectibles} response]
collectible (first collectibles)]
(if collectible (if collectible
{:fx [[:dispatch [:wallet/store-last-collectible-details collectible]]]} {:db (update-in db [:wallet :ui :collectible :details] merge-skipping-empty-values collectible)}
(log/error "failed to get collectible details" (log/error "failed to get collectible details"
{:event :wallet/get-collectible-details-done {:event :wallet/get-collectible-details-done
:response response}))))) :response response})))))
(rf/reg-event-fx (rf/reg-event-fx
:wallet/clear-last-collectible-details :wallet/clear-collectible-details
(fn [{:keys [db]}] (fn [{:keys [db]}]
{:db (update-in db [:wallet] dissoc :last-collectible-details :last-collectible-aspect-ratio)})) {:db (update-in db [:wallet :ui] dissoc :collectible)}))
(rf/reg-event-fx :wallet/trigger-share-collectible (rf/reg-event-fx :wallet/trigger-share-collectible
(fn [_ [{:keys [title uri]}]] (fn [_ [{:keys [title uri]}]]

View File

@ -43,18 +43,6 @@
(is (match? result-db expected-db)))))) (is (match? result-db expected-db))))))
(deftest store-last-collectible-details-test
(testing "store-last-collectible-details"
(let [db {:wallet {}}
last-collectible {:description "Pandaria"
:image-url "https://..."}
expected-db {:wallet {:last-collectible-details {:description "Pandaria"
:image-url "https://..."}}}
effects (events/store-last-collectible-details {:db db}
[last-collectible])
result-db (:db effects)]
(is (match? result-db expected-db)))))
(deftest request-new-collectibles-for-account-from-signal-test (deftest request-new-collectibles-for-account-from-signal-test
(testing "request new collectibles for account from signal" (testing "request new collectibles for account from signal"
(let [db {:wallet {}} (let [db {:wallet {}}

View File

@ -8,12 +8,11 @@
(def ^:private link-card-space 28) (def ^:private link-card-space 28)
(defn view (defn view
[] [{:keys [collectible-data]}]
(let [window-width (rf/sub [:dimensions/window-width]) (let [window-width (rf/sub [:dimensions/window-width])
item-width (- (/ window-width 2) link-card-space) item-width (- (/ window-width 2) link-card-space)
{:keys [collectible-data]} (rf/sub [:wallet/last-collectible-details]) link-card-container-style (style/link-card item-width)
link-card-container-style (style/link-card item-width) collectible-about {:cards []}]
collectible-about {:cards []}]
[:<> [:<>
[rn/view {:style style/title} [rn/view {:style style/title}
[quo/text [quo/text

View File

@ -21,27 +21,25 @@
:container-style style/traits-item}]) :container-style style/traits-item}])
(defn- traits-section (defn- traits-section
[] [traits]
(let [traits (rf/sub [:wallet/last-collectible-details-traits])] [rn/view
(when (pos? (count traits)) [quo/section-label
[rn/view {:section (i18n/label :t/traits)
[quo/section-label :container-style style/traits-title-container}]
{:section (i18n/label :t/traits) [rn/flat-list
:container-style style/traits-title-container}] {:render-fn trait-item
[rn/flat-list :data traits
{:render-fn trait-item :key :collectibles-list
:data traits :key-fn :id
:key :collectibles-list :num-columns 2
:key-fn :id :content-container-style style/traits-container}]])
:num-columns 2
:content-container-style style/traits-container}]])))
(defn- info (defn- info
[] [{:keys [chain-id account]}]
(let [chain-id (rf/sub [:wallet/last-collectible-details-chain-id]) (let [{:keys [network-name]} (rf/sub [:wallet/network-details-by-chain-id chain-id])
{:keys [network-name]} (rf/sub [:wallet/network-details-by-chain-id chain-id]) subtitle (some-> network-name
subtitle (string/capitalize (name (or network-name ""))) name
{:keys [name emoji color]} (rf/sub [:wallet/last-collectible-details-owner])] string/capitalize)]
[rn/view {:style style/info-container} [rn/view {:style style/info-container}
[rn/view {:style style/account} [rn/view {:style style/account}
[quo/data-item [quo/data-item
@ -49,10 +47,10 @@
:status :default :status :default
:size :default :size :default
:title (i18n/label :t/account-title) :title (i18n/label :t/account-title)
:subtitle name
:emoji emoji
:subtitle-type :account :subtitle-type :account
:customization-color color}]] :subtitle (:name account)
:emoji (:emoji account)
:customization-color (:color account)}]]
[rn/view {:style style/network} [rn/view {:style style/network}
[quo/data-item [quo/data-item
{:subtitle-type :network {:subtitle-type :network
@ -64,7 +62,12 @@
:subtitle subtitle}]]])) :subtitle subtitle}]]]))
(defn view (defn view
[] [collectible]
[:<> (let [owner-account (rf/sub [:wallet/collectible-details-owner collectible])
[info] traits (-> collectible :collectible-data :traits)]
[traits-section]]) [:<>
[info
{:chain-id (-> collectible :id :contract-id :chain-id)
:account owner-account}]
(when (pos? (count traits))
[traits-section traits])]))

View File

@ -5,9 +5,9 @@
[status-im.contexts.wallet.collectible.tabs.overview.view :as overview])) [status-im.contexts.wallet.collectible.tabs.overview.view :as overview]))
(defn view (defn view
[{:keys [selected-tab]}] [{:keys [selected-tab collectible]}]
(case selected-tab (case selected-tab
:overview [overview/view] :overview [overview/view collectible]
:about [about/view] :about [about/view collectible]
:activity [activity/view] :activity [activity/view]
nil)) nil))

View File

@ -12,7 +12,7 @@
:balance :balance
js/parseInt)) js/parseInt))
(def ^:const supported-collectible-types (def supported-collectible-types
#{"image/jpeg" #{"image/jpeg"
"image/gif" "image/gif"
"image/bmp" "image/bmp"

View File

@ -76,11 +76,10 @@
(defn navigate-back-and-clear-collectible (defn navigate-back-and-clear-collectible
[] []
(rf/dispatch [:navigate-back]) (rf/dispatch [:navigate-back])
(js/setTimeout #(rf/dispatch [:wallet/clear-last-collectible-details]) 700)) (rf/dispatch [:wallet/clear-collectible-details]))
(defn animated-header (defn animated-header
[{:keys [scroll-amount title-opacity page-nav-type picture title description theme [{:keys [scroll-amount title-opacity page-nav-type picture title description theme id]}]
id]}]
(let [blur-amount (header-animations/use-blur-amount scroll-amount) (let [blur-amount (header-animations/use-blur-amount scroll-amount)
layer-opacity (header-animations/use-layer-opacity layer-opacity (header-animations/use-layer-opacity
scroll-amount scroll-amount
@ -149,8 +148,9 @@
(let [title-ref (rn/use-ref-atom nil) (let [title-ref (rn/use-ref-atom nil)
set-title-ref (rn/use-callback #(reset! title-ref %)) set-title-ref (rn/use-callback #(reset! title-ref %))
animation-shared-element-id (rf/sub [:animation-shared-element-id]) animation-shared-element-id (rf/sub [:animation-shared-element-id])
collectible-owner (rf/sub [:wallet/last-collectible-details-owner]) collectible-owner (rf/sub [:wallet/collectible-details-owner collectible])
aspect-ratio (rf/sub [:wallet/last-collectible-aspect-ratio]) aspect-ratio (rf/sub [:wallet/collectible-aspect-ratio])
gradient-color (rf/sub [:wallet/collectible-gradient-color])
{:keys [id {:keys [id
preview-url preview-url
collection-data collection-data
@ -181,35 +181,36 @@
[rn/view [rn/view
[gradient-layer preview-uri] [gradient-layer preview-uri]
[quo/expanded-collectible [quo/expanded-collectible
{:aspect-ratio aspect-ratio {:aspect-ratio aspect-ratio
:image-src preview-uri :image-src preview-uri
:container-style (style/preview-container) :container-style (style/preview-container)
:counter (utils/collectible-owned-counter total-owned) :gradient-color-index gradient-color
:native-ID (when (= animation-shared-element-id token-id) :shared-element) :counter (utils/collectible-owned-counter total-owned)
:supported-file? (utils/supported-file? (:animation-media-type collectible-data)) :native-ID (when (= animation-shared-element-id token-id) :shared-element)
:on-press (fn [] :supported-file? (utils/supported-file? (:animation-media-type collectible-data))
(if svg? :on-press (fn []
(js/alert "Can't visualize SVG images in lightbox") (if svg?
(rf/dispatch (js/alert "Can't visualize SVG images in lightbox")
[:lightbox/navigate-to-lightbox (rf/dispatch
token-id [:lightbox/navigate-to-lightbox
{:images [collectible-image] token-id
:index 0 {:images [collectible-image]
:on-options-press #(rf/dispatch [:show-bottom-sheet :index 0
{:content :on-options-press #(rf/dispatch [:show-bottom-sheet
(fn [] {:content
[options-drawer/view (fn []
{:name collectible-name [options-drawer/view
:image {:name collectible-name
preview-uri :image
:id id}])}])}]))) preview-uri
:on-collectible-load (fn [] :id id}])}])}])))
;; We need to delay the measurement because the :on-collectible-load (fn []
;; navigation has an animation ;; We need to delay the measurement because the
(js/setTimeout ;; navigation has an animation
#(some-> @title-ref (js/setTimeout
(oops/ocall "measureInWindow" set-title-bottom)) #(some-> @title-ref
300))}]] (oops/ocall "measureInWindow" set-title-bottom))
300))}]]
[rn/view {:style (style/background-color theme)} [rn/view {:style (style/background-color theme)}
[header collectible-name collection-name collection-image set-title-ref] [header collectible-name collection-name collection-image set-title-ref]
[cta-buttons [cta-buttons
@ -225,39 +226,43 @@
:default-active @selected-tab :default-active @selected-tab
:on-change on-tab-change :on-change on-tab-change
:data tabs-data}] :data tabs-data}]
[tabs/view {:selected-tab @selected-tab}]]])))) [tabs/view
{:selected-tab @selected-tab
:collectible collectible}]]]))))
(defn- get-title-bottom-y-position
[y-element-position element-height]
(let [{:keys [top]} (safe-area/get-insets)
title-height -56]
(+ y-element-position
element-height
title-height
(when platform/ios? (* top -2)))))
(defn view (defn view
[_] [_]
(let [{:keys [top]} (safe-area/get-insets) (let [{:keys [top]} (safe-area/get-insets)
theme (quo.theme/use-theme) theme (quo.theme/use-theme)
title-bottom-coord (rn/use-ref-atom 0) title-bottom-coord (rn/use-ref-atom 0)
set-title-bottom (rn/use-callback set-title-bottom (rn/use-callback
(fn [_ y _ height] (fn [_ y _ height]
(reset! title-bottom-coord (reset! title-bottom-coord
(+ y (get-title-bottom-y-position y height))))
height scroll-amount (reanimated/use-shared-value 0)
-56 title-opacity (reanimated/use-shared-value 0)
(when platform/ios? {:keys [collection-data
(* top -2)))))) collectible-data
scroll-amount (reanimated/use-shared-value 0) preview-url]
title-opacity (reanimated/use-shared-value 0) :as collectible} (rf/sub [:wallet/collectible-details])]
collectible (rf/sub [:wallet/last-collectible-details])
{:keys [preview-url
collection-data
collectible-data]} collectible
{preview-uri :uri} preview-url
{collectible-name :name} collectible-data
{collection-name :name} collection-data]
[rn/view {:style (style/background-color theme)} [rn/view {:style (style/background-color theme)}
[animated-header [animated-header
{:id (:id collectible) {:id (:id collectible)
:scroll-amount scroll-amount :scroll-amount scroll-amount
:title-opacity title-opacity :title-opacity title-opacity
:page-nav-type :title-description :page-nav-type :title-description
:picture preview-uri :picture (:uri preview-url)
:title collectible-name :title (:name collectible-data)
:description collection-name :description (:name collection-data)
:theme theme}] :theme theme}]
[reanimated/scroll-view [reanimated/scroll-view
{:style (style/scroll-view top) {:style (style/scroll-view top)

View File

@ -13,25 +13,34 @@
[{:keys [preview-url collection-data collectible-data total-owned on-press on-long-press] [{:keys [preview-url collection-data collectible-data total-owned on-press on-long-press]
:as collectible} :as collectible}
index] index]
(let [on-press-fn (rn/use-callback #(when on-press (let [gradient-color (keyword (str "gradient-" (inc (mod index 5))))
(on-press collectible %))) on-press-fn (rn/use-callback
on-long-press-fn (rn/use-callback #(when on-long-press (fn [aspect-ratio]
(on-long-press collectible)))] (when (fn? on-press)
(on-press {:collectible (dissoc collectible :on-press :on-long-press)
:aspect-ratio aspect-ratio
:gradient-color gradient-color}))))
on-long-press-fn (rn/use-callback
(fn [aspect-ratio]
(when on-long-press
(on-long-press
(dissoc collectible :on-press :on-long-press)
aspect-ratio))))]
[quo/collectible-list-item [quo/collectible-list-item
{:type :card {:type :card
:image-src (:uri preview-url) :image-src (:uri preview-url)
:avatar-image-src (:image-url collection-data) :avatar-image-src (:image-url collection-data)
:collectible-name (:name collection-data) :collectible-name (:name collectible-data)
: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 gradient-color
:counter (utils/collectible-owned-counter total-owned) :counter (utils/collectible-owned-counter total-owned)
:container-style style/collectible-container :container-style style/collectible-container
:on-press on-press-fn :on-press on-press-fn
:on-long-press on-long-press-fn}])) :on-long-press on-long-press-fn}]))
(defn view (defn view
[{:keys [collectibles filtered? on-end-reached [{:keys [collectibles filtered? on-end-reached on-collectible-press
on-collectible-press current-account-address on-collectible-long-press]}] current-account-address on-collectible-long-press]}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
no-results-match-query? (and filtered? (empty? collectibles))] no-results-match-query? (and filtered? (empty? collectibles))]
(cond (cond

View File

@ -17,9 +17,7 @@
:name (:name collectible-data) :name (:name collectible-data)
:image (:uri preview-url)}])}])) :image (:uri preview-url)}])}]))
(defn- on-collectible-press (def on-collectible-press #(rf/dispatch [:wallet/navigate-to-collectible-details %]))
[{:keys [id]} aspect-ratio]
(rf/dispatch [:wallet/get-collectible-details id aspect-ratio]))
(defn view (defn view
[{:keys [selected-tab]}] [{:keys [selected-tab]}]

View File

@ -32,7 +32,7 @@
{:collectibles collectibles {:collectibles collectibles
:filtered? search-performed? :filtered? search-performed?
:on-end-reached #(rf/dispatch [:wallet/request-collectibles-for-current-viewing-account]) :on-end-reached #(rf/dispatch [:wallet/request-collectibles-for-current-viewing-account])
:on-collectible-press (fn [collectible] :on-collectible-press (fn [{:keys [collectible]}]
(rf/dispatch [:wallet/set-collectible-to-send (rf/dispatch [:wallet/set-collectible-to-send
{:collectible collectible {:collectible collectible
:current-screen :screen/wallet.select-asset}]))}])) :current-screen :screen/wallet.select-asset}]))}]))

View File

@ -20,8 +20,7 @@
(defn- preview-url (defn- preview-url
[{{collectible-image-url :image-url [{{collectible-image-url :image-url
animation-url :animation-url animation-url :animation-url
animation-media-type :animation-media-type} :collectible-data animation-media-type :animation-media-type} :collectible-data}]
{collection-image-url :image-url} :collection-data}]
(cond (cond
(svg-animation? animation-url animation-media-type) (svg-animation? animation-url animation-media-type)
{:uri animation-url {:uri animation-url
@ -34,7 +33,7 @@
{:uri collectible-image-url} {:uri collectible-image-url}
:else :else
{:uri collection-image-url})) {:uri nil}))
(defn add-collectibles-preview-url (defn add-collectibles-preview-url
[collectibles] [collectibles]
@ -91,35 +90,36 @@
current-account-collectibles)))) current-account-collectibles))))
(re-frame/reg-sub (re-frame/reg-sub
:wallet/last-collectible-details :wallet/collectible
:<- [:wallet] :<- [:wallet/ui]
(fn [wallet] :-> :collectible)
(let [last-collectible (:last-collectible-details wallet)]
(assoc last-collectible :preview-url (preview-url last-collectible)))))
(re-frame/reg-sub (re-frame/reg-sub
:wallet/last-collectible-aspect-ratio :wallet/collectible-details
:<- [:wallet] :<- [:wallet/collectible]
(fn [wallet]
(:last-collectible-aspect-ratio wallet)))
(re-frame/reg-sub
:wallet/last-collectible-details-chain-id
:<- [:wallet/last-collectible-details]
(fn [collectible] (fn [collectible]
(get-in collectible [:id :contract-id :chain-id]))) (as-> collectible $
(:details $)
(assoc $ :preview-url (preview-url $)))))
(re-frame/reg-sub (re-frame/reg-sub
:wallet/last-collectible-details-traits :wallet/collectible-aspect-ratio
:<- [:wallet/last-collectible-details] :<- [:wallet/collectible]
(fn [collectible] (fn [collectible]
(get-in collectible [:collectible-data :traits]))) (:aspect-ratio collectible 1)))
(re-frame/reg-sub (re-frame/reg-sub
:wallet/last-collectible-details-owner :wallet/collectible-gradient-color
:<- [:wallet/last-collectible-details] :<- [:wallet/collectible]
:<- [:wallet] (fn [collectible]
(fn [[collectible wallet]] (:gradient-color collectible :gradient-1)))
(let [address (:address (first (:ownership collectible)))
account (get-in wallet [:accounts address])] (re-frame/reg-sub
account))) :wallet/collectible-details-owner
:<- [:wallet/accounts]
(fn [accounts [_ collectible]]
(let [collectible-address (-> collectible :ownership first :address)]
(some #(when (= (:address %) collectible-address)
%)
accounts))))

View File

@ -2,49 +2,25 @@
(:require (:require
[cljs.test :refer [is testing]] [cljs.test :refer [is testing]]
[re-frame.db :as rf-db] [re-frame.db :as rf-db]
status-im.subs.root [status-im.subs.root]
status-im.subs.wallet.collectibles [status-im.subs.wallet.collectibles]
[test-helpers.unit :as h] [test-helpers.unit :as h]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(def ^:private traits (def collectible-owner-wallet
[{:trait-type "Background" {:ui {:collectible {:details {:ownership [{:address "0x1"}]}}}
:value "Gradient 5" :accounts {"0x1" {:address "0x1"
:display-type "" :name "account 1"
:max-value ""} :color "army"}}})
{:trait-type "Skin"
:value "Pale"
:display-type ""
:max-value ""}
{:trait-type "Clothes"
:value "Naked"
:display-type ""
:max-value ""}])
(def ^:private collectible-owner-wallet (h/deftest-sub :wallet/collectible-details-owner
{:last-collectible-details {:ownership [{:address "0x1"}]}
:accounts {"0x1" {:name "account 1"
:color "army"}}})
(h/deftest-sub :wallet/last-collectible-details-chain-id
[sub-name]
(testing "correct chain-id of the last collectible should be returned"
(swap! rf-db/app-db assoc-in [:wallet :last-collectible-details :id :contract-id :chain-id] "1")
(let [result (rf/sub [sub-name])]
(is (= "1" result)))))
(h/deftest-sub :wallet/last-collectible-details-traits
[sub-name]
(testing "correct traits of the last collectible should be returned"
(swap! rf-db/app-db assoc-in [:wallet :last-collectible-details :collectible-data :traits] traits)
(let [result (rf/sub [sub-name])]
(is (= traits result)))))
(h/deftest-sub :wallet/last-collectible-details-owner
[sub-name] [sub-name]
(testing "correct owner of the last collectible should be returned" (testing "correct owner of the last collectible should be returned"
(swap! rf-db/app-db assoc-in [:wallet] collectible-owner-wallet) (swap! rf-db/app-db assoc :wallet collectible-owner-wallet)
(let [result (rf/sub [sub-name])] (let [collectible (-> collectible-owner-wallet :ui :collectible :details)
(is (= {:name "account 1" result (rf/sub [sub-name collectible])]
:color "army"} (is (= {:name "account 1"
:color "army"
:address "0x1"
:network-preferences-names #{}}
result))))) result)))))