mirror of
https://github.com/status-im/status-mobile.git
synced 2025-02-14 09:36:33 +00:00
fix(avatars) - Blinking all over the app and improvement in UX (#21782)
* Change multiple uses of `merge` to `assoc` in flat-list wrapper * Fix fast-image wrapper to avoid blinking * merge -> assoc usage in chat-item component * Fix avatar not being updated when the user changes their username * Fix scalable avatar issues - On Android, the border was inconsistent depending on the theme. - Fix animation, simplify the calculation, and now it matches designs * Avoid blinking by using rn/memo
This commit is contained in:
parent
b88bbda3c5
commit
32a3f85694
@ -1,40 +1,79 @@
|
||||
(ns react-native.fast-image
|
||||
(:require
|
||||
["react-native-fast-image" :as FastImage]
|
||||
[clojure.string :as string]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]))
|
||||
[reagent.core :as reagent]
|
||||
[utils.transforms :as transforms]))
|
||||
|
||||
(def fast-image-class (reagent/adapt-react-class ^js FastImage))
|
||||
(defn- build-source
|
||||
[source]
|
||||
(if (string? source)
|
||||
{:uri source
|
||||
:priority :high}
|
||||
source))
|
||||
|
||||
(defn placeholder
|
||||
[style child]
|
||||
[rn/view {:style (merge style {:flex 1 :justify-content :center :align-items :center})}
|
||||
child])
|
||||
(defn- remove-port
|
||||
[source]
|
||||
(cond
|
||||
(string? source) (string/replace-first source #":\d+" "")
|
||||
(:uri source) (some-> source
|
||||
:uri
|
||||
(string/replace-first #":\d+" ""))
|
||||
:else source))
|
||||
|
||||
(defn fast-image
|
||||
(defn- placeholder
|
||||
[{:keys [style fallback-content error? loaded?]}]
|
||||
[rn/view
|
||||
{:style (assoc style
|
||||
:flex 1
|
||||
:justify-content :center
|
||||
:align-items :center)}
|
||||
(cond
|
||||
(and error? fallback-content) fallback-content
|
||||
error? [rn/text "X"]
|
||||
(not loaded?) [rn/activity-indicator {:animating true}])])
|
||||
|
||||
;; We cannot use hooks since `reactify-component` seems to ignore the functional compiler
|
||||
(defn- internal-fast-image
|
||||
[_]
|
||||
(let [loaded? (reagent/atom false)
|
||||
error? (reagent/atom false)]
|
||||
(fn [{:keys [source fallback-content] :as props}]
|
||||
[fast-image-class
|
||||
(merge
|
||||
props
|
||||
{:source (if (string? source)
|
||||
{:uri source
|
||||
:priority :high}
|
||||
source)
|
||||
:on-error (fn [e]
|
||||
(when-let [on-error (:on-error props)]
|
||||
(on-error e))
|
||||
(reset! error? true))
|
||||
:on-load (fn [e]
|
||||
(when-let [on-load (:on-load props)]
|
||||
(on-load e))
|
||||
(reset! loaded? true)
|
||||
(reset! error? false))})
|
||||
(let [loaded? (reagent/atom false)
|
||||
error? (reagent/atom false)
|
||||
on-image-error (fn [event on-error]
|
||||
(when (fn? on-error) (on-error event))
|
||||
(reset! error? true))
|
||||
on-image-loaded (fn [event on-load]
|
||||
(when (fn? on-load) (on-load event))
|
||||
(reset! loaded? true)
|
||||
(reset! error? false))]
|
||||
(fn [{:keys [source fallback-content on-error on-load] :as props}]
|
||||
[:> FastImage
|
||||
(assoc props
|
||||
:source (build-source source)
|
||||
:on-error #(on-image-error % on-error)
|
||||
:on-load #(on-image-loaded % on-load))
|
||||
(when (or @error? (not @loaded?))
|
||||
[placeholder (:style props)
|
||||
(if @error?
|
||||
(or fallback-content [rn/text "X"])
|
||||
(when-not @loaded?
|
||||
[rn/activity-indicator {:animating true}]))])])))
|
||||
[placeholder
|
||||
{:style (js->clj (:style props))
|
||||
:fallback-content fallback-content
|
||||
:error? @error?
|
||||
:loaded? @loaded?}])])))
|
||||
|
||||
(defn- compare-props
|
||||
[old-props new-props]
|
||||
(let [old-props-clj (transforms/js->clj old-props)
|
||||
new-props-clj (transforms/js->clj new-props)
|
||||
old-source (some-> old-props-clj
|
||||
:source
|
||||
remove-port)
|
||||
new-source (some-> new-props-clj
|
||||
:source
|
||||
remove-port)]
|
||||
(and (= old-source new-source)
|
||||
(= (dissoc old-props-clj :source) (dissoc new-props-clj :source)))))
|
||||
|
||||
(def fast-image
|
||||
(-> internal-fast-image
|
||||
(reagent/reactify-component)
|
||||
(rn/memo compare-props)
|
||||
(reagent/adapt-react-class)))
|
||||
|
@ -23,19 +23,22 @@
|
||||
(when f
|
||||
(f data index))))
|
||||
|
||||
(defn dissoc-custom-props
|
||||
[props]
|
||||
(dissoc props :data :header :footer :empty-component :separator :render-fn :key-fn :on-drag-end-fn))
|
||||
|
||||
(defn base-list-props
|
||||
[{:keys [key-fn render-fn empty-component header footer separator data render-data on-drag-end-fn]
|
||||
[{:keys [key-fn data render-fn empty-component header footer separator render-data on-drag-end-fn]
|
||||
:as props}]
|
||||
(merge
|
||||
{:data (to-array data)}
|
||||
(when key-fn {:keyExtractor (wrap-key-fn key-fn)})
|
||||
(when render-fn {:renderItem (wrap-render-fn render-fn render-data)})
|
||||
(when separator {:ItemSeparatorComponent (fn [] (reagent/as-element separator))})
|
||||
(when empty-component {:ListEmptyComponent (fn [] (reagent/as-element empty-component))})
|
||||
(when header {:ListHeaderComponent (reagent/as-element header)})
|
||||
(when footer {:ListFooterComponent (reagent/as-element footer)})
|
||||
(when on-drag-end-fn {:onDragEnd (wrap-on-drag-end-fn on-drag-end-fn)})
|
||||
(dissoc props :data :header :footer :empty-component :separator :render-fn :key-fn :on-drag-end-fn)))
|
||||
(cond-> {:data (to-array data)}
|
||||
key-fn (assoc :keyExtractor (wrap-key-fn key-fn))
|
||||
render-fn (assoc :renderItem (wrap-render-fn render-fn render-data))
|
||||
separator (assoc :ItemSeparatorComponent (fn [] (reagent/as-element separator)))
|
||||
empty-component (assoc :ListEmptyComponent (fn [] (reagent/as-element empty-component)))
|
||||
header (assoc :ListHeaderComponent (reagent/as-element header))
|
||||
footer (assoc :ListFooterComponent (reagent/as-element footer))
|
||||
on-drag-end-fn (assoc :onDragEnd (wrap-on-drag-end-fn on-drag-end-fn))
|
||||
:always (merge (dissoc-custom-props props))))
|
||||
|
||||
(defn flat-list
|
||||
[props]
|
||||
|
@ -1,11 +1,10 @@
|
||||
(ns status-im.common.scalable-avatar.style)
|
||||
|
||||
(defn wrapper
|
||||
[{:keys [scale margin-top margin border-color]}]
|
||||
[{:transform [{:scale scale}]
|
||||
:margin-top margin-top
|
||||
:margin-left margin
|
||||
:margin-bottom margin}
|
||||
{:border-width 4
|
||||
:border-color border-color
|
||||
:border-radius 100}])
|
||||
[border-color scale]
|
||||
[{:transform-origin "bottom left"
|
||||
:border-width 4
|
||||
:border-color border-color
|
||||
:border-radius 100
|
||||
:transform [{:scale 1} {:translate-y 4}]}
|
||||
{:transform [{:scale scale} {:translate-y 4}]}])
|
||||
|
@ -2,30 +2,14 @@
|
||||
(:require [quo.core :as quo]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.common.scalable-avatar.style :as style]))
|
||||
(def scroll-animation-input-range [0 50])
|
||||
(def header-extrapolation-option
|
||||
{:extrapolateLeft "clamp"
|
||||
:extrapolateRight "clamp"})
|
||||
|
||||
(defn f-avatar
|
||||
(def scroll-range #js [0 48])
|
||||
(def scale-range #js [1 0.4])
|
||||
|
||||
(defn view
|
||||
[{:keys [scroll-y full-name online? profile-picture customization-color border-color]}]
|
||||
(let [image-scale-animation (reanimated/interpolate scroll-y
|
||||
scroll-animation-input-range
|
||||
[1 0.4]
|
||||
header-extrapolation-option)
|
||||
image-top-margin-animation (reanimated/interpolate scroll-y
|
||||
scroll-animation-input-range
|
||||
[0 20]
|
||||
header-extrapolation-option)
|
||||
image-side-margin-animation (reanimated/interpolate scroll-y
|
||||
scroll-animation-input-range
|
||||
[-4 -20]
|
||||
header-extrapolation-option)]
|
||||
[reanimated/view
|
||||
{:style (style/wrapper {:scale image-scale-animation
|
||||
:margin-top image-top-margin-animation
|
||||
:margin image-side-margin-animation
|
||||
:border-color border-color})}
|
||||
(let [image-scale (reanimated/interpolate scroll-y scroll-range scale-range :clamp)]
|
||||
[reanimated/view {:style (style/wrapper border-color image-scale)}
|
||||
[quo/user-avatar
|
||||
{:full-name full-name
|
||||
:online? online?
|
||||
@ -34,7 +18,3 @@
|
||||
:ring? true
|
||||
:customization-color customization-color
|
||||
:size :big}]]))
|
||||
|
||||
(defn view
|
||||
[props]
|
||||
[:f> f-avatar props])
|
||||
|
@ -10,14 +10,12 @@
|
||||
[quo/documentation-drawers
|
||||
{:title (i18n/label :t/forgot-your-password-info-title)
|
||||
:shell? shell?}
|
||||
[rn/view
|
||||
{:style style/container}
|
||||
[rn/view {:style style/container}
|
||||
[quo/text {:size :paragraph-2} (i18n/label :t/forgot-your-password-info-description)]
|
||||
|
||||
[rn/view {:style style/step-container}
|
||||
[quo/step {:in-blur-view? shell?} 1]
|
||||
[rn/view
|
||||
{:style style/step-content}
|
||||
[rn/view {:style style/step-content}
|
||||
[quo/text {:size :paragraph-2 :weight :semi-bold}
|
||||
(i18n/label :t/forgot-your-password-info-remove-app)]
|
||||
[quo/text {:size :paragraph-2} (i18n/label :t/forgot-your-password-info-remove-app-description)]]]
|
||||
@ -33,10 +31,8 @@
|
||||
|
||||
[rn/view {:style style/step-container}
|
||||
[quo/step {:in-blur-view? shell?} 3]
|
||||
[rn/view
|
||||
{:style style/step-content}
|
||||
[rn/view
|
||||
{:style style/step-title}
|
||||
[rn/view {:style style/step-content}
|
||||
[rn/view {:style style/step-title}
|
||||
[quo/text {:size :paragraph-2} (str (i18n/label :t/sign-up) " ")]
|
||||
[quo/text {:size :paragraph-2 :weight :semi-bold}
|
||||
(i18n/label :t/forgot-your-password-info-signup-with-key)]]
|
||||
@ -45,8 +41,7 @@
|
||||
|
||||
[rn/view {:style style/step-container}
|
||||
[quo/step {:in-blur-view? shell?} 4]
|
||||
[rn/view
|
||||
{:style style/step-content}
|
||||
[rn/view {:style style/step-content}
|
||||
[quo/text {:size :paragraph-2 :weight :semi-bold}
|
||||
(i18n/label :t/forgot-your-password-info-create-new-password)]
|
||||
[quo/text {:size :paragraph-2}
|
||||
|
@ -276,8 +276,7 @@
|
||||
|
||||
(defn chat-user
|
||||
[item]
|
||||
[rn/view
|
||||
{:style (merge style/container {:margin-horizontal 0})}
|
||||
[rn/view {:style (assoc style/container :margin-horizontal 0)}
|
||||
[chat-item item]])
|
||||
|
||||
(defn chat-list-item
|
||||
|
@ -62,8 +62,8 @@
|
||||
:on-end-reached #(re-frame/dispatch [:chat/show-more-chats])
|
||||
:keyboard-should-persist-taps :always
|
||||
:data items
|
||||
:render-fn (fn [item]
|
||||
(chat-list-item/chat-list-item item theme))
|
||||
:render-data {:theme theme}
|
||||
:render-fn chat-list-item/chat-list-item
|
||||
:scroll-event-throttle 8
|
||||
:content-container-style {:padding-bottom
|
||||
shell.constants/floating-shell-button-height
|
||||
@ -94,8 +94,7 @@
|
||||
{:selected-tab :tab/contacts
|
||||
:tab->content (empty-state-content theme)}]
|
||||
[rn/section-list
|
||||
{:ref (when (not-empty items)
|
||||
set-scroll-ref)
|
||||
{:ref (when (seq items) set-scroll-ref)
|
||||
:key-fn :public-key
|
||||
:get-item-layout get-item-layout
|
||||
:content-inset-adjustment-behavior :never
|
||||
@ -108,8 +107,8 @@
|
||||
:sticky-section-headers-enabled false
|
||||
:render-section-header-fn contact-list/contacts-section-header
|
||||
:render-section-footer-fn contact-list/contacts-section-footer
|
||||
:render-fn (fn [data]
|
||||
(contact-item-render data theme))
|
||||
:render-data {:theme theme}
|
||||
:render-fn contact-item-render
|
||||
:scroll-event-throttle 8
|
||||
:on-scroll #(common.banner/set-scroll-shared-value
|
||||
{:scroll-input (oops/oget % "nativeEvent.contentOffset.y")
|
||||
|
@ -19,7 +19,7 @@
|
||||
{:d "M20 20V0H0C11 0 20 9 20 20Z"
|
||||
:fill background-color}]])
|
||||
|
||||
(defn f-view
|
||||
(defn view
|
||||
[{:keys [scroll-y customization-color theme]}]
|
||||
(let [background-color (colors/resolve-color customization-color theme 40)
|
||||
opacity-animation (reanimated/interpolate scroll-y
|
||||
@ -30,7 +30,3 @@
|
||||
[reanimated/view {:style (style/radius-container opacity-animation)}
|
||||
[left-radius background-color]
|
||||
[right-radius background-color]]]))
|
||||
|
||||
(defn view
|
||||
[props]
|
||||
[:f> f-view props])
|
||||
|
@ -1,11 +1,11 @@
|
||||
(ns status-im.contexts.profile.settings.header.style)
|
||||
(ns status-im.contexts.profile.settings.header.style
|
||||
(:require [quo.foundations.colors :as colors]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
(def avatar-row-wrapper
|
||||
{:display :flex
|
||||
:padding-left 20
|
||||
{:padding-left 20
|
||||
:padding-right 12
|
||||
:margin-top -60
|
||||
:margin-bottom -4
|
||||
:margin-top -65
|
||||
:align-items :flex-end
|
||||
:justify-content :space-between
|
||||
:flex-direction :row})
|
||||
@ -21,3 +21,9 @@
|
||||
{:opacity opacity-animation
|
||||
:flex-direction :row
|
||||
:justify-content :space-between})
|
||||
|
||||
(defn avatar-border-color
|
||||
[theme]
|
||||
(if platform/android?
|
||||
colors/neutral-80-opa-80 ;; Fix is not needed because Android doesn't use blur
|
||||
(colors/theme-colors colors/border-avatar-light colors/neutral-80-opa-80 theme)))
|
||||
|
@ -1,6 +1,5 @@
|
||||
(ns status-im.contexts.profile.settings.header.view
|
||||
(:require [quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
[quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[status-im.common.scalable-avatar.view :as avatar]
|
||||
@ -11,49 +10,46 @@
|
||||
[status-im.contexts.profile.utils :as profile.utils]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn- on-state-dropdown-press
|
||||
[]
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:shell? true
|
||||
:theme :dark
|
||||
:content visibility-sheet/view}]))
|
||||
|
||||
(defn view
|
||||
[{:keys [scroll-y]}]
|
||||
(let [theme (quo.theme/use-theme)
|
||||
app-theme (rf/sub [:theme])
|
||||
{:keys [public-key emoji-hash bio] :as profile} (rf/sub [:profile/profile-with-image])
|
||||
online? (rf/sub [:visibility-status-updates/online?
|
||||
public-key])
|
||||
status (rf/sub
|
||||
[:visibility-status-updates/visibility-status-update
|
||||
public-key])
|
||||
customization-color (rf/sub [:profile/customization-color])
|
||||
full-name (profile.utils/displayed-name profile)
|
||||
profile-picture (profile.utils/photo profile)
|
||||
{:keys [status-title status-icon]} (header.utils/visibility-status-type-data status)
|
||||
border-theme app-theme]
|
||||
(let [app-theme (rf/sub [:theme])
|
||||
{:keys [public-key emoji-hash bio]
|
||||
:as profile} (rf/sub [:profile/profile-with-image])
|
||||
online? (rf/sub [:visibility-status-updates/online? public-key])
|
||||
status (rf/sub [:visibility-status-updates/visibility-status-update public-key])
|
||||
customization-color (rf/sub [:profile/customization-color])
|
||||
full-name (profile.utils/displayed-name profile)
|
||||
profile-picture (profile.utils/photo profile)
|
||||
{:keys [status-title
|
||||
status-icon]} (header.utils/visibility-status-type-data status)]
|
||||
[:<>
|
||||
[header.shape/view
|
||||
{:scroll-y scroll-y
|
||||
:customization-color customization-color
|
||||
:theme theme}]
|
||||
:customization-color customization-color}]
|
||||
[rn/view {:style style/avatar-row-wrapper}
|
||||
[avatar/view
|
||||
{:scroll-y scroll-y
|
||||
:display-name full-name
|
||||
:full-name full-name
|
||||
:online? online?
|
||||
:border-color (colors/theme-colors colors/border-avatar-light
|
||||
colors/neutral-80-opa-80
|
||||
border-theme)
|
||||
:border-color (style/avatar-border-color app-theme)
|
||||
:customization-color customization-color
|
||||
:profile-picture profile-picture}]
|
||||
[rn/view {:style {:margin-bottom 4}}
|
||||
[quo/dropdown
|
||||
{:background :blur
|
||||
:size :size-32
|
||||
:type :outline
|
||||
:icon? true
|
||||
:no-icon-color? true
|
||||
:icon-name status-icon
|
||||
:on-press #(rf/dispatch [:show-bottom-sheet
|
||||
{:shell? true
|
||||
:theme :dark
|
||||
:content (fn [] [visibility-sheet/view])}])}
|
||||
status-title]]]
|
||||
[quo/dropdown
|
||||
{:background :blur
|
||||
:size :size-32
|
||||
:type :outline
|
||||
:icon? true
|
||||
:no-icon-color? true
|
||||
:icon-name status-icon
|
||||
:on-press on-state-dropdown-press}
|
||||
status-title]]
|
||||
[quo/page-top
|
||||
{:title-accessibility-label :username
|
||||
:emoji-dash emoji-hash
|
||||
|
@ -1,7 +1,7 @@
|
||||
(ns status-im.contexts.profile.settings.view
|
||||
(:require [oops.core :as oops]
|
||||
[quo.core :as quo]
|
||||
[quo.theme :as quo.theme]
|
||||
[quo.theme]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[react-native.safe-area :as safe-area]
|
||||
|
Loading…
x
Reference in New Issue
Block a user