Fixes for the Community Channel avatar component (#16379)

- Community channels may not have an emoji, and in such cases, it should fall
  back to a single letter. The component was recently updated in Figma after
  this concern was brought up to designers.
- The default (fallback) color to be used in the list of community channels
  should be the community color, not a default "primary-50" as was before. It's
  a subtle change because the colors are rendered at 10% transparency.
- Rewrote the channel-avatar component to follow guidelines.

Fixes:

- https://github.com/status-im/status-mobile/issues/16332
- https://github.com/status-im/status-mobile/issues/16327
This commit is contained in:
Icaro Motta 2023-07-07 14:16:50 +00:00 committed by GitHub
parent d76e64b1bb
commit 17f5ad79f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 234 additions and 116 deletions

View File

@ -1,50 +0,0 @@
(ns quo2.components.avatars.channel-avatar
(:require [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[quo2.theme :as theme]
[react-native.core :as rn]))
(defn channel-avatar
[{:keys [big? locked? emoji-background-color emoji]}]
(let [lock-exists? locked?
dark? (theme/dark?)]
[rn/view
{:style {:width (if big? 32 24)
:height (if big? 32 24)
:border-radius (if big? 32 24)
:justify-content :center
:align-items :center
:background-color emoji-background-color}}
[rn/view
{:style {:display :flex
:justify-content :center
:align-items :center}}
[text/text
{:size (if big?
:paragraph-1
:label)} emoji]
(when lock-exists?
[rn/view
{:style {:position :absolute
:left (if big?
14
8)
:top (if big?
15
8)
:background-color (if dark?
colors/neutral-90
colors/white)
:border-radius 15
:padding 2}}
[icons/icon
(if locked?
:main-icons/locked
:main-icons/unlocked)
{:color (if dark?
colors/neutral-40
colors/neutral-50)
:container-style {:width 12
:height 12}
:size 12}]])]]))

View File

@ -0,0 +1,35 @@
(ns quo2.components.avatars.channel-avatar.component-spec
(:require [quo2.components.avatars.channel-avatar.view :as component]
[test-helpers.component :as h]))
(h/describe "Channel Avatar"
(h/test "default render"
(h/render [component/view])
(h/is-truthy (h/query-by-label-text :initials))
(h/is-null (h/query-by-label-text :emoji))
(h/is-null (h/query-by-label-text :lock)))
(h/test "with emoji, no lock set, large size"
(let [emoji "🍓"]
(h/render [component/view {:emoji emoji :size :size/l}])
(h/is-null (h/query-by-label-text :initials))
(h/is-truthy (h/query-by-text emoji))
(h/is-null (h/query-by-label-text :lock))))
(h/test "locked"
(h/render [component/view {:locked? true}])
(h/is-truthy (h/query-by-label-text :lock)))
(h/test "unlocked"
(h/render [component/view {:locked? false}])
(h/is-truthy (h/query-by-label-text :lock)))
(h/test "no emoji, smaller size"
(h/render [component/view {:full-name "Status Mobile"}])
(h/is-truthy (h/query-by-text "S")))
(h/test "no emoji, large size"
(h/render [component/view
{:full-name "Status Mobile"
:size :size/l}])
(h/is-truthy (h/query-by-text "SM"))))

View File

@ -0,0 +1,28 @@
(ns quo2.components.avatars.channel-avatar.style
(:require [quo2.foundations.colors :as colors]))
(def lock-icon-size 12)
(defn outer-container
[{:keys [size color]}]
(let [size (if (= size :size/l) 32 24)]
{:width size
:height size
:border-radius size
:justify-content :center
:align-items :center
:background-color (colors/theme-alpha color 0.1 0.1)}))
(defn lock-container
[size]
(let [distance (if (= size :size/l) 20 12)]
{:position :absolute
:left distance
:top distance
:background-color (colors/theme-colors colors/white colors/neutral-95)
:border-radius (* 2 lock-icon-size)
:padding 2}))
(def lock-icon
{:width lock-icon-size
:height lock-icon-size})

View File

@ -0,0 +1,60 @@
(ns quo2.components.avatars.channel-avatar.view
(:require [clojure.string :as string]
[quo2.components.avatars.channel-avatar.style :as style]
[quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
utils.string))
(defn- initials
[full-name size color]
(let [amount-initials (if (= size :size/l) 2 1)]
[text/text
{:accessibility-label :initials
:size :paragraph-2
:weight :semi-bold
:style {:color color}}
(utils.string/get-initials full-name amount-initials)]))
(defn- lock
[locked? size]
;; When `locked?` is nil, we must not display the unlocked icon.
(when (boolean? locked?)
[rn/view
{:accessibility-label :lock
:style (style/lock-container size)}
[icons/icon (if locked? :i/locked :i/unlocked)
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40)
:container-style style/lock-icon
:size 12}]]))
(defn view
"Options:
:size - keyword (default nil) - Container size, for the moment,
only :size/l (meaning large) is supported.
:emoji - string (default nil)
:customization-color - color (default nil) If the component is used for a
community channel, then the default color should be the community custom
color.
:locked? - nil/bool (default nil) - When true/false display a locked/unlocked
icon respectively. When nil does not show icon.
:full-name - string (default nil) - When :emoji is blank, this value will be
used to extract the initials.
"
[{:keys [size emoji customization-color locked? full-name]}]
[rn/view
{:accessibility-label :channel-avatar
:style (style/outer-container {:size size :color customization-color})}
(if (string/blank? emoji)
[initials full-name size customization-color]
[text/text
{:accessibility-label :emoji
:size (if (= size :size/l) :paragraph-1 :label)}
(string/trim emoji)])
[lock locked? size]])

View File

@ -1,22 +1,9 @@
(ns quo2.components.avatars.user-avatar.view
(:require [clojure.string :as string]
[quo2.components.avatars.user-avatar.style :as style]
(:require [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 trim-whitespace [s] (string/join " " (string/split (string/trim s) #"\s+")))
(defn- extract-initials
[full-name amount-initials]
(let [upper-case-first-letter (comp string/upper-case first)
names-list (string/split (trim-whitespace full-name) " ")]
(if (= (first names-list) "")
""
(->> names-list
(map upper-case-first-letter)
(take amount-initials)
(string/join)))))
[react-native.fast-image :as fast-image]
utils.string))
(defn initials-avatar
[{:keys [full-name size draw-ring? customization-color]}]
@ -29,7 +16,7 @@
{:style style/initials-avatar-text
:size font-size
:weight :semi-bold}
(extract-initials full-name amount-initials)]]))
(utils.string/get-initials full-name amount-initials)]]))
(def valid-ring-sizes #{:big :medium :small})

View File

@ -1,6 +1,6 @@
(ns quo2.components.list-items.channel
(:require [quo2.components.avatars.channel-avatar :as channel-avatar]
[quo2.components.common.unread-grey-dot.view :refer [unread-grey-dot]]
(:require [quo2.components.avatars.channel-avatar.view :as channel-avatar]
[quo2.components.common.unread-grey-dot.view :as unread-grey-dot]
[quo2.components.counter.counter :as quo2.counter]
[quo2.components.icon :as quo2.icons]
[quo2.components.markdown.text :as quo2.text]
@ -13,10 +13,11 @@
(defn list-item
[{:keys [locked? mentions-count unread-messages?
muted? is-active-channel? emoji channel-color]
:or {channel-color colors/primary-50}
muted? is-active-channel? emoji channel-color
default-color]
:as props}]
(let [standard-props (apply dissoc props custom-props)
(let [channel-color (or channel-color default-color)
standard-props (apply dissoc props custom-props)
name-text (:name props)]
[rn/touchable-opacity standard-props
[rn/view
@ -36,11 +37,12 @@
:align-items :center}
:accessible true
:accessibility-label :chat-name-text}
[channel-avatar/channel-avatar
{:big? true
:locked? locked?
:emoji-background-color (colors/theme-alpha channel-color 0.1 0.1)
:emoji emoji}]
[channel-avatar/view
{:size :size/l
:locked? locked?
:full-name (:name props)
:customization-color channel-color
:emoji emoji}]
[quo2.text/text
{:style (cond-> {:margin-left 12}
(and (not locked?) muted?)
@ -63,4 +65,4 @@
mentions-count]]
unread-messages?
[unread-grey-dot :unviewed-messages-public])])]]))
[unread-grey-dot/unread-grey-dot :unviewed-messages-public])])]]))

View File

@ -2,7 +2,7 @@
(:refer-clojure :exclude [filter])
(:require
quo2.components.avatars.account-avatar
quo2.components.avatars.channel-avatar
quo2.components.avatars.channel-avatar.view
quo2.components.avatars.group-avatar
quo2.components.avatars.icon-avatar
quo2.components.avatars.user-avatar.view
@ -121,7 +121,7 @@
;;;; AVATAR
(def account-avatar quo2.components.avatars.account-avatar/account-avatar)
(def channel-avatar quo2.components.avatars.channel-avatar/channel-avatar)
(def channel-avatar quo2.components.avatars.channel-avatar.view/view)
(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.view/user-avatar)

View File

@ -49,7 +49,7 @@
(oops/oget event "nativeEvent.layout.y"))
(defn- channel-chat-item
[community-id {chat-id :id muted? :muted? :as chat}]
[community-id community-color {chat-id :id muted? :muted? :as chat}]
(let [sheet-content [actions/chat-actions
(assoc chat :chat-type constants/community-chat-type)
false]
@ -58,12 +58,13 @@
[rn/view {:key chat-id :style {:margin-top 4}}
[quo/channel-list-item
(assoc chat
:default-color community-color
:on-long-press #(rf/dispatch [:show-bottom-sheet channel-sheet-data])
:muted? (or muted?
(rf/sub [:chat/check-channel-muted? community-id chat-id])))]]))
(defn channel-list-component
[{:keys [on-category-layout community-id on-first-channel-height-changed]}
[{:keys [on-category-layout community-id community-color on-first-channel-height-changed]}
channels-list]
[rn/view
{:on-layout #(on-first-channel-height-changed
@ -91,7 +92,7 @@
:chevron-position :left}])
(when-not collapsed?
(into [rn/view {:style {:padding-horizontal 8 :padding-bottom 8}}]
(map #(channel-chat-item community-id %))
(map #(channel-chat-item community-id community-color %))
chats))]))])
(defn request-to-join-text
@ -262,7 +263,7 @@
description])
(defn community-content
[{:keys [name description joined tags id]
[{:keys [name description joined tags color id]
:as community}
pending?
{:keys [on-category-layout on-first-channel-height-changed]}]
@ -280,6 +281,7 @@
[channel-list-component
{:on-category-layout on-category-layout
:community-id id
:community-color color
:on-first-channel-height-changed on-first-channel-height-changed}
(add-handlers-to-categorized-chats id chats-by-category)]]))

View File

@ -1,53 +1,65 @@
(ns status-im2.contexts.quo-preview.avatars.channel-avatar
(:require [quo2.components.avatars.channel-avatar :as quo2]
(:require [quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im2.contexts.quo-preview.preview :as preview]))
(def descriptor
[{:label "Big?"
:key :big?
:type :boolean}
{:label "Avatar color"
:key :emoji-background-color
:type :text}
{:label "Avatar color"
[{:label "Size:"
:key :size
:type :select
:options [{:key :size/l :value "Large"}
{:key :default :value "Default"}]}
{:label "Emoji:"
:key :emoji
:type :text}
{:label "is Locked?"
:key :locked?
{:label "Full name:"
:key :full-name
:type :text}
(preview/customization-color-option)
{:label "Locked state:"
:key :locked-state
:type :select
:options [{:key nil
:value "None"}
{:key false
:options [{:key :not-set
:value "Not set"}
{:key :unlocked
:value "Unlocked"}
{:key true
{:key :locked
:value "Locked"}]}])
(defn cool-preview
[]
(let [state (reagent/atom {:big? true
:locked? nil
:emoji "🍑"
:emoji-background-color :gray})]
(let [state (reagent/atom {:size :size/l
:locked-state :not-set
:emoji "🍑"
:full-name "Some channel"
:customization-color :blue})]
(fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150}
[rn/view {:flex 1}
[preview/customizer state descriptor]]
[rn/view
{:padding-vertical 60
:flex-direction :row
:justify-content :center}
[quo2/channel-avatar @state]]]])))
(let [customization-color (colors/custom-color-by-theme (:customization-color @state) 50 60)
locked? (case (:locked-state @state)
:not-set nil
:unlocked false
:locked true
nil)]
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:style {:padding-bottom 150}}
[rn/view {:style {:flex 1}}
[preview/customizer state descriptor]]
[rn/view
{:style {:padding-vertical 60
:flex-direction :row
:justify-content :center}}
[quo/channel-avatar
(assoc @state
:locked? locked?
:customization-color customization-color)]]]]))))
(defn preview-channel-avatar
[]
[rn/view
{:background-color (colors/theme-colors colors/white
colors/neutral-90)
:flex 1}
{:style {:background-color (colors/theme-colors colors/white colors/neutral-95)
:flex 1}}
[rn/flat-list
{:flex 1
:keyboard-should-persist-taps :always

View File

@ -202,6 +202,20 @@
:select [customizer-select descriptor]
nil)]))])
(defn customization-color-option
([]
(customization-color-option {}))
([opts]
(merge {:label "Custom color:"
:key :customization-color
:type :select
:options (->> colors/customization
keys
sort
(map (fn [k]
{:key k :value (string/capitalize (name k))})))}
opts)))
(comment
[{:label "Show error:"
:key :error

View File

@ -41,8 +41,8 @@
{:style {:flex-direction :row
:align-items :center}}
[quo/channel-avatar
{:emoji (:emoji community-channel)
:emoji-background-color (colors/alpha color-50 0.1)}]
{:emoji (:emoji community-channel)
:customization-color color-50}]
[quo/text
{:size :paragraph-2
:weight :medium

View File

@ -49,3 +49,12 @@
[s m r]
(when (string? s)
(string/replace s m r)))
(defn get-initials
"Returns `n` number of initial letters from `s`, all uppercased."
[s n]
(let [words (-> s str string/trim (string/split #"\s+"))]
(->> words
(take n)
(map (comp string/upper-case str first))
string/join)))

View File

@ -0,0 +1,19 @@
(ns utils.string-test
(:require [cljs.test :refer [are deftest]]
utils.string))
(deftest get-initials-test
(are [expected input amount-initials]
(= expected (utils.string/get-initials input amount-initials))
"" nil 0
"" nil 1
"" "" 0
"" "ab" 0
"" "" 1
"A" "ab" 1
"A" " ab " 1
"A" "a b" 1
"A" "ab" 2
"AB" "a b" 2
"ABC" "a b c d" 3
"ABC" " a b c d" 3))