From 8478ac74ab9162759e70cfcfbbfd8e6c7e3ef780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulises=20Manuel=20C=C3=A1rdenas?= <90291778+ulisesmac@users.noreply.github.com> Date: Wed, 8 Mar 2023 11:07:37 -0600 Subject: [PATCH] Refactor user-avatar & add tests --- src/quo2/components/avatars/user_avatar.cljs | 138 ------------------ .../avatars/user_avatar/component_spec.cljs | 129 ++++++++++++++++ .../components/avatars/user_avatar/style.cljs | 83 +++++++++++ .../components/avatars/user_avatar/view.cljs | 61 ++++++++ .../components/list_items/preview_list.cljs | 2 +- src/quo2/components/list_items/user_list.cljs | 2 +- .../components/messages/system_message.cljs | 2 +- src/quo2/components/navigation/page_nav.cljs | 2 +- .../components/profile/profile_card/view.cljs | 2 +- .../profile/select_profile/view.cljs | 2 +- src/quo2/core.cljs | 4 +- src/quo2/core_spec.cljs | 3 +- .../quo_preview/avatars/user_avatar.cljs | 18 ++- src/test_helpers/component.cljs | 16 ++ 14 files changed, 312 insertions(+), 152 deletions(-) delete mode 100644 src/quo2/components/avatars/user_avatar.cljs create mode 100644 src/quo2/components/avatars/user_avatar/component_spec.cljs create mode 100644 src/quo2/components/avatars/user_avatar/style.cljs create mode 100644 src/quo2/components/avatars/user_avatar/view.cljs diff --git a/src/quo2/components/avatars/user_avatar.cljs b/src/quo2/components/avatars/user_avatar.cljs deleted file mode 100644 index 84e2933e4d..0000000000 --- a/src/quo2/components/avatars/user_avatar.cljs +++ /dev/null @@ -1,138 +0,0 @@ -(ns quo2.components.avatars.user-avatar - (:require [clojure.string :as string] - [quo2.components.markdown.text :as text] - [quo2.foundations.colors :as colors] - [quo2.theme :refer [dark?]] - [react-native.core :as rn] - [react-native.fast-image :as fast-image])) - -(def sizes - {:big {:outer 80 - :inner 72 - :status-indicator 20 - :status-indicator-border 4 - :font-size :heading-1} - :medium {:outer 48 - :inner 44 - :status-indicator 12 - :status-indicator-border 2 - :font-size :paragraph-1} - :small {:outer 32 - :inner 28 - :status-indicator 12 - :status-indicator-border 2 - :font-size :paragraph-2} - :xs {:outer 24 - :inner 24 - :status-indicator 0 - :status-indicator-border 0 - :font-size :paragraph-2} - :xxs {:outer 20 - :inner 20 - :status-indicator 0 - :status-indicator-border 0 - :font-size :label} - :xxxs {:outer 16 - :inner 16 - :status-indicator 0 - :status-indicator-border 0 - :font-size :label}}) - -(defn dot-indicator - [{:keys [size online? ring? dark?]}] - (let [dimensions (get-in sizes [size :status-indicator]) - border-width (get-in sizes [size :status-indicator-border]) - right (case size - :big 2 - :medium 0 - :small -2 - 0) - bottom (case size - :big (if ring? -1 2) - :medium (if ring? 0 -2) - :small -2 - 0)] - [rn/view - {:style {:background-color (if online? - colors/success-50 - colors/neutral-40) - :width dimensions - :height dimensions - :border-width border-width - :border-radius dimensions - :border-color (if dark? - colors/neutral-100 - colors/white) - :position :absolute - :bottom bottom - :right right}}])) - -(defn initials-style - [inner-dimensions outer-dimensions] - {:position :absolute - :top (/ (- outer-dimensions inner-dimensions) 2) - :left (/ (- outer-dimensions inner-dimensions) 2) - :width inner-dimensions - :height inner-dimensions - :border-radius inner-dimensions - :justify-content :center - :align-items :center - :background-color (colors/custom-color-by-theme :turquoise 50 60)}) - -(defn outer-styles - [outer-dimensions] - {:width outer-dimensions - :height outer-dimensions - :border-radius outer-dimensions}) - -(def one-initial-letter-sizes #{:xs :xxs :xxxs}) -(def valid-ring-sizes #{:big :medium :small}) - -(defn initials-avatar - [{:keys [full-name size inner-dimensions outer-dimensions]}] - (let [amount-initials (if (one-initial-letter-sizes size) 1 2) - initials (as-> full-name $ - (string/split $ " ") - (map (comp string/upper-case first) $) - (take amount-initials $) - (string/join $)) - font-size (get-in sizes [size :font-size])] - [rn/view {:style (initials-style inner-dimensions outer-dimensions)} - [text/text - {:style {:color colors/white-opa-70} - :weight :semi-bold - :size font-size} - initials]])) - -(defn user-avatar - "If no `profile-picture` is given, draws the initials based on the `full-name` and - uses `ring-background` to display the ring behind the initials when given. Otherwise, - shows the profile picture which already comes with the ring drawn over it." - [{:keys [full-name status-indicator? online? size profile-picture ring-background] - :or {status-indicator? true - online? true - size :big}}] - (let [full-name (or full-name "empty name") - draw-ring? (and ring-background (valid-ring-sizes size)) - outer-dimensions (get-in sizes [size :outer]) - inner-dimensions (get-in sizes [size (if draw-ring? :inner :outer)])] - [rn/view - {:style (outer-styles outer-dimensions) - :accessibility-label :user-avatar} - ;; The `profile-picture` already has the ring in it - (when-let [image (or profile-picture ring-background)] - [fast-image/fast-image - {:style (outer-styles outer-dimensions) - :source image}]) - (when-not profile-picture - [initials-avatar - {:full-name full-name - :size size - :inner-dimensions inner-dimensions - :outer-dimensions outer-dimensions}]) - (when status-indicator? - [dot-indicator - {:size size - :online? online? - :ring? draw-ring? - :dark? (dark?)}])])) diff --git a/src/quo2/components/avatars/user_avatar/component_spec.cljs b/src/quo2/components/avatars/user_avatar/component_spec.cljs new file mode 100644 index 0000000000..a8aec064b0 --- /dev/null +++ b/src/quo2/components/avatars/user_avatar/component_spec.cljs @@ -0,0 +1,129 @@ +(ns quo2.components.avatars.user-avatar.component-spec + (:require [quo2.components.avatars.user-avatar.view :as user-avatar] + [test-helpers.component :as h])) + +(defonce mock-picture (js/require "../resources/images/mock2/user_picture_male4.png")) + +(h/describe "user avatar" + (h/test "Default render" + (h/render [user-avatar/user-avatar]) + (h/is-truthy (h/get-by-label-text :user-avatar)) + (h/is-truthy (h/get-by-text "EN"))) + + (h/describe "Profile picture" + (h/test "Renders" + (h/render + [user-avatar/user-avatar {:profile-picture mock-picture}]) + (h/is-truthy (h/get-by-label-text :profile-picture))) + + (h/test "Renders even if `:full-name` is passed" + (h/render + [user-avatar/user-avatar + {:profile-picture mock-picture + :full-name "New User1"}]) + (h/is-truthy (h/get-by-label-text :profile-picture)) + (h/is-null (h/query-by-label-text :initials-avatar))) + + (h/describe "Status indicator" + (h/test "Render" + (h/render + [user-avatar/user-avatar + {:profile-picture mock-picture + :status-indicator? true}]) + (h/is-truthy (h/get-by-label-text :profile-picture)) + (h/is-truthy (h/get-by-label-text :status-indicator))) + + (h/test "Do not render" + (h/render + [user-avatar/user-avatar + {:profile-picture mock-picture + :status-indicator? false}]) + (h/is-truthy (h/get-by-label-text :profile-picture)) + (h/is-null (h/query-by-label-text :status-indicator))))) + + (h/describe "Initials Avatar" + (h/describe "Render initials" + (letfn [(user-avatar-component [size] + [user-avatar/user-avatar + {:full-name "New User" + :size size}])] + (h/describe "Two letters" + (h/test "Size :big" + (h/render (user-avatar-component :big)) + (h/is-truthy (h/get-by-text "NU"))) + + (h/test "Size :medium" + (h/render (user-avatar-component :medium)) + (h/is-truthy (h/get-by-text "NU"))) + + (h/test "Size :small" + (h/render (user-avatar-component :small)) + (h/is-truthy (h/get-by-text "NU")))) + + (h/describe "One letter" + (h/test "Size :xs" + (h/render (user-avatar-component :xs)) + (h/is-truthy (h/get-by-text "N"))) + + (h/test "Size :xxs" + (h/render (user-avatar-component :xxs)) + (h/is-truthy (h/get-by-text "N"))) + + (h/test "Size :xxxs" + (h/render (user-avatar-component :xxxs)) + (h/is-truthy (h/get-by-text "N")))))) + + (h/describe "Render ring" + (letfn [(user-avatar-component [size] + [user-avatar/user-avatar + {:full-name "New User" + :ring-background mock-picture + :size size}])] + (h/describe "Passed and drawn" + (h/test "Size :big" + (h/render (user-avatar-component :big)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-truthy (h/get-by-label-text :ring-background))) + + (h/test "Size :medium" + (h/render (user-avatar-component :medium)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-truthy (h/get-by-label-text :ring-background))) + + (h/test "Size :small" + (h/render (user-avatar-component :small)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-truthy (h/get-by-label-text :ring-background)))) + + (h/describe "Passed and not drawn (because of invalid size for ring)" + (h/test "Size :xs" + (h/render (user-avatar-component :xs)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-null (h/query-by-label-text :ring-background))) + + (h/test "Size :xxs" + (h/render (user-avatar-component :xxs)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-null (h/query-by-label-text :ring-background))) + + (h/test "Size :xxxs" + (h/render (user-avatar-component :xxxs)) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-null (h/query-by-label-text :ring-background)))))) + + (h/describe "Status indicator" + (h/test "Render" + (h/render + [user-avatar/user-avatar + {:full-name "Test User" + :status-indicator? true}]) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-truthy (h/get-by-label-text :status-indicator))) + + (h/test "Do not render" + (h/render + [user-avatar/user-avatar + {:full-name "Test User" + :status-indicator? false}]) + (h/is-truthy (h/get-by-label-text :initials-avatar)) + (h/is-null (h/query-by-label-text :status-indicator)))))) diff --git a/src/quo2/components/avatars/user_avatar/style.cljs b/src/quo2/components/avatars/user_avatar/style.cljs new file mode 100644 index 0000000000..b04e6e5bb1 --- /dev/null +++ b/src/quo2/components/avatars/user_avatar/style.cljs @@ -0,0 +1,83 @@ +(ns quo2.components.avatars.user-avatar.style + (:require [quo2.foundations.colors :as colors])) + +(def sizes + {:big {:outer 80 + :inner 72 + :status-indicator 20 + :status-indicator-border 4 + :font-size :heading-1} + :medium {:outer 48 + :inner 44 + :status-indicator 12 + :status-indicator-border 2 + :font-size :paragraph-1} + :small {:outer 32 + :inner 28 + :status-indicator 12 + :status-indicator-border 2 + :font-size :paragraph-2} + :xs {:outer 24 + :inner 24 + :status-indicator 0 + :status-indicator-border 0 + :font-size :paragraph-2} + :xxs {:outer 20 + :inner 20 + :status-indicator 0 + :status-indicator-border 0 + :font-size :label} + :xxxs {:outer 16 + :inner 16 + :status-indicator 0 + :status-indicator-border 0 + :font-size :label}}) + +(defn outer + [size] + (let [dimensions (get-in sizes [size :outer])] + {:width dimensions + :height dimensions + :border-radius dimensions})) + +(defn initials-avatar + [size draw-ring? customization-color] + (let [outer-dimensions (get-in sizes [size :outer]) + inner-dimensions (get-in sizes [size (if draw-ring? :inner :outer)])] + {:position :absolute + :top (/ (- outer-dimensions inner-dimensions) 2) + :left (/ (- outer-dimensions inner-dimensions) 2) + :width inner-dimensions + :height inner-dimensions + :border-radius inner-dimensions + :justify-content :center + :align-items :center + :background-color (colors/custom-color-by-theme customization-color 50 60)})) + +(def initials-avatar-text + {:color colors/white-opa-70}) + +(defn dot + [size online? ring?] + (let [background (if online? colors/success-50 colors/neutral-40) + dimensions (get-in sizes [size :status-indicator]) + border-width (get-in sizes [size :status-indicator-border]) + right (case size + :big 2 + :medium 0 + :small -2 + 0) + bottom (case size + :big (if ring? -1 2) + :medium (if ring? 0 -2) + :small -2 + 0)] + {:position :absolute + :bottom bottom + :right right + :width dimensions + :height dimensions + :border-width border-width + :border-radius dimensions + :border-color (colors/theme-colors colors/white colors/neutral-100) + :background-color background})) diff --git a/src/quo2/components/avatars/user_avatar/view.cljs b/src/quo2/components/avatars/user_avatar/view.cljs new file mode 100644 index 0000000000..37658ef1e6 --- /dev/null +++ b/src/quo2/components/avatars/user_avatar/view.cljs @@ -0,0 +1,61 @@ +(ns quo2.components.avatars.user-avatar.view + (:require [clojure.string :as string] + [quo2.components.avatars.user-avatar.style :as style] + [quo2.components.markdown.text :as text] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn- extract-initials + [full-name amount-initials] + (let [upper-case-first-letter (comp string/upper-case first) + names-list (string/split full-name " ")] + (->> names-list + (map upper-case-first-letter) + (take amount-initials) + (string/join)))) + +(defn initials-avatar + [{:keys [full-name size draw-ring? customization-color]}] + (let [font-size (get-in style/sizes [size :font-size]) + amount-initials (if (#{:xs :xxs :xxxs} size) 1 2)] + [rn/view + {:accessibility-label :initials-avatar + :style (style/initials-avatar size draw-ring? customization-color)} + [text/text + {:style style/initials-avatar-text + :size font-size + :weight :semi-bold} + (extract-initials full-name amount-initials)]])) + +(def valid-ring-sizes #{:big :medium :small}) + +(defn user-avatar + "If no `profile-picture` is given, draws the initials based on the `full-name` and + uses `ring-background` to display the ring behind the initials when given. Otherwise, + shows the `profile-picture` which already comes with the ring drawn." + [{:keys [full-name status-indicator? online? size profile-picture ring-background + customization-color] + :or {status-indicator? true + online? true + size :big + customization-color :turquoise}}] + (let [full-name (or full-name "empty name") + draw-ring? (and ring-background (valid-ring-sizes size)) + outer-styles (style/outer size)] + [rn/view {:style outer-styles :accessibility-label :user-avatar} + ;; The `profile-picture` already has the ring in it + (when-let [image (or profile-picture ring-background)] + [fast-image/fast-image + {:accessibility-label (if draw-ring? :ring-background :profile-picture) + :style outer-styles + :source image}]) + (when-not profile-picture + [initials-avatar + {:full-name full-name + :size size + :draw-ring? draw-ring? + :customization-color customization-color}]) + (when status-indicator? + [rn/view + {:accessibility-label :status-indicator + :style (style/dot size online? draw-ring?)}])])) diff --git a/src/quo2/components/list_items/preview_list.cljs b/src/quo2/components/list_items/preview_list.cljs index 907c7a78fb..cf717936c3 100644 --- a/src/quo2/components/list_items/preview_list.cljs +++ b/src/quo2/components/list_items/preview_list.cljs @@ -1,5 +1,5 @@ (ns quo2.components.list-items.preview-list - (:require [quo2.components.avatars.user-avatar :as user-avatar] + (:require [quo2.components.avatars.user-avatar.view :as user-avatar] [quo2.components.icon :as quo2.icons] [quo2.components.markdown.text :as quo2.text] [quo2.foundations.colors :as colors] diff --git a/src/quo2/components/list_items/user_list.cljs b/src/quo2/components/list_items/user_list.cljs index c63dd1f5a6..41c487f1cd 100644 --- a/src/quo2/components/list_items/user_list.cljs +++ b/src/quo2/components/list_items/user_list.cljs @@ -1,6 +1,6 @@ (ns quo2.components.list-items.user-list (:require [react-native.core :as rn] - [quo2.components.avatars.user-avatar :as user-avatar] + [quo2.components.avatars.user-avatar.view :as user-avatar] [quo2.components.markdown.text :as text] [quo2.components.icon :as icons] [quo2.foundations.colors :as colors] diff --git a/src/quo2/components/messages/system_message.cljs b/src/quo2/components/messages/system_message.cljs index 9087b48b5e..9900f3e05a 100644 --- a/src/quo2/components/messages/system_message.cljs +++ b/src/quo2/components/messages/system_message.cljs @@ -1,6 +1,6 @@ (ns quo2.components.messages.system-message (:require [quo2.components.avatars.icon-avatar :as icon-avatar] - [quo2.components.avatars.user-avatar :as user-avatar] + [quo2.components.avatars.user-avatar.view :as user-avatar] [quo2.components.markdown.text :as text] [quo2.foundations.colors :as colors] [quo2.theme :as theme] diff --git a/src/quo2/components/navigation/page_nav.cljs b/src/quo2/components/navigation/page_nav.cljs index a858a81f47..9f786d0e2f 100644 --- a/src/quo2/components/navigation/page_nav.cljs +++ b/src/quo2/components/navigation/page_nav.cljs @@ -1,6 +1,6 @@ (ns quo2.components.navigation.page-nav (:require [clojure.string :as string] - [quo2.components.avatars.user-avatar :as user-avatar] + [quo2.components.avatars.user-avatar.view :as user-avatar] [quo2.components.buttons.button :as button] [quo2.components.icon :as icons] [quo2.components.markdown.text :as text] diff --git a/src/quo2/components/profile/profile_card/view.cljs b/src/quo2/components/profile/profile_card/view.cljs index 62e3b6e115..fb158f869f 100644 --- a/src/quo2/components/profile/profile_card/view.cljs +++ b/src/quo2/components/profile/profile_card/view.cljs @@ -6,7 +6,7 @@ [quo2.foundations.colors :as colors] [quo2.components.markdown.text :as text] [quo2.components.buttons.button :as button] - [quo2.components.avatars.user-avatar :as user-avatar] + [quo2.components.avatars.user-avatar.view :as user-avatar] [quo2.components.profile.profile-card.style :as style])) (defn profile-card diff --git a/src/quo2/components/profile/select_profile/view.cljs b/src/quo2/components/profile/select_profile/view.cljs index 40ad56e10e..e627caa246 100644 --- a/src/quo2/components/profile/select_profile/view.cljs +++ b/src/quo2/components/profile/select_profile/view.cljs @@ -3,7 +3,7 @@ [quo2.components.profile.select-profile.style :as style] [react-native.core :as rn] [quo2.components.markdown.text :as text] - [quo2.components.avatars.user-avatar :as user-avatar] + [quo2.components.avatars.user-avatar.view :as user-avatar] [reagent.core :as reagent])) (defn- on-change-handler diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index e35029ee58..6379a12fee 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -5,7 +5,7 @@ quo2.components.avatars.channel-avatar quo2.components.avatars.group-avatar quo2.components.avatars.icon-avatar - quo2.components.avatars.user-avatar + quo2.components.avatars.user-avatar.view quo2.components.avatars.wallet-user-avatar quo2.components.banners.banner.view quo2.components.buttons.button @@ -102,7 +102,7 @@ (def channel-avatar quo2.components.avatars.channel-avatar/channel-avatar) (def group-avatar quo2.components.avatars.group-avatar/group-avatar) (def icon-avatar quo2.components.avatars.icon-avatar/icon-avatar) -(def user-avatar quo2.components.avatars.user-avatar/user-avatar) +(def user-avatar quo2.components.avatars.user-avatar.view/user-avatar) (def wallet-user-avatar quo2.components.avatars.wallet-user-avatar/wallet-user-avatar) ;;;; BANNER diff --git a/src/quo2/core_spec.cljs b/src/quo2/core_spec.cljs index 25341dd211..4c8a624307 100644 --- a/src/quo2/core_spec.cljs +++ b/src/quo2/core_spec.cljs @@ -1,5 +1,6 @@ (ns quo2.core-spec - (:require [quo2.components.banners.banner.component-spec] + (:require [quo2.components.avatars.user-avatar.component-spec] + [quo2.components.banners.banner.component-spec] [quo2.components.buttons.--tests--.buttons-component-spec] [quo2.components.counter.--tests--.counter-component-spec] [quo2.components.dividers.--tests--.divider-label-component-spec] diff --git a/src/status_im2/contexts/quo_preview/avatars/user_avatar.cljs b/src/status_im2/contexts/quo_preview/avatars/user_avatar.cljs index 0ac7273036..bacc82d0b7 100644 --- a/src/status_im2/contexts/quo_preview/avatars/user_avatar.cljs +++ b/src/status_im2/contexts/quo_preview/avatars/user_avatar.cljs @@ -1,5 +1,5 @@ (ns status-im2.contexts.quo-preview.avatars.user-avatar - (:require [quo2.components.avatars.user-avatar :as quo2] + (:require [quo2.components.avatars.user-avatar.view :as quo2] [quo2.foundations.colors :as colors] [react-native.core :as rn] [reagent.core :as reagent] @@ -22,6 +22,13 @@ :value "xx Small"} {:key :xxxs :value "xxx Small"}]} + {:label "Customization color:" + :key :customization-color + :type :select + :options (map (fn [[color-kw _]] + {:key color-kw + :value (name color-kw)}) + colors/customization)} {:label "Online status" :key :online? :type :boolean} @@ -46,10 +53,11 @@ (defn cool-preview [] - (let [state (reagent/atom {:full-name "A Y" - :status-indicator? true - :online? true - :size :medium})] + (let [state (reagent/atom {:full-name "A Y" + :status-indicator? true + :online? true + :size :medium + :customization-color :blue})] (fn [] [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} [rn/view {:padding-bottom 150} diff --git a/src/test_helpers/component.cljs b/src/test_helpers/component.cljs index 3d6f02f1eb..790bdc7035 100644 --- a/src/test_helpers/component.cljs +++ b/src/test_helpers/component.cljs @@ -30,10 +30,18 @@ [text] (rtl/screen.getByText text)) +(defn find-by-text + [text] + (rtl/screen.findByText text)) + (defn get-by-label-text [label] (rtl/screen.getByLabelText (name label))) +(defn query-by-label-text + [label] + (rtl/screen.queryByLabelText (name label))) + (defn get-by-translation-text [keyword] (get-by-text (str "tx:" (name keyword)))) @@ -55,3 +63,11 @@ (js/jest.advanceTimersByTime time-ms)) (def mock-fn js/jest.fn) + +(defn is-truthy + [element] + (.toBeTruthy (js/expect element))) + +(defn is-null + [element] + (.toBeNull (js/expect element)))