chat list optimization

This commit is contained in:
andrey 2021-04-13 16:14:50 +02:00
parent db848a1f19
commit 073371c4ed
No known key found for this signature in database
GPG Key ID: 89B67245FD2F0272
12 changed files with 237 additions and 197 deletions

View File

@ -45,14 +45,16 @@
;; i18n ignores nil value, leading to misleading messages
(into {} (for [[k v] options] [k (or v default-option-value)])))
(defn label
([path] (label path {}))
(defn label-fn
([path] (label-fn path {}))
([path options]
(if (exists? (.t i18n))
(let [options (update options :amount label-number)]
(.t i18n (name path) (clj->js (label-options options))))
(name path))))
(def label (memoize label-fn))
(defn label-pluralize [count path & options]
(if (exists? (.t i18n))
(.p i18n count (name path) (clj->js options))

View File

@ -10,19 +10,24 @@
;;TODO REWORK THIS NAMESPACE
(def get-name-first-char
(memoize
(fn [name]
;; TODO: for now we check if the first letter is a #
;; which means it is most likely a public chat and
;; use the second letter if that is the case
;; a broader refactoring should clean up upstream params
;; for default-chat-icon
(string/capitalize (if (and (= "#" (first name))
(< 1 (count name)))
(second name)
(first name))))))
(defn default-chat-icon [name styles]
(when-not (string/blank? name)
[react/view (:default-chat-icon styles)
[react/text {:style (:default-chat-icon-text styles)}
;; TODO: for now we check if the first letter is a #
;; which means it is most likely a public chat and
;; use the second letter if that is the case
;; a broader refactoring should clean up upstream params
;; for default-chat-icon
(string/capitalize (if (and (= "#" (first name))
(< 1 (count name)))
(second name)
(first name)))]]))
(get-name-first-char name)]]))
(defn chat-icon-view
[chat-id group-chat name styles]

View File

@ -26,27 +26,25 @@
:else
colors/black))
(defn icon
([name] (icon name nil))
(defn memo-icon-fn
([name] (memo-icon-fn name nil))
([name {:keys [color resize-mode container-style
accessibility-label width height]
:or {accessibility-label :icon}}]
^{:key name}
[react/view
{:style (or
container-style
{:width (or width 24)
:height (or height 24)})
:accessibility-label accessibility-label}
[react/image {:style (cond-> {:width (or width 24)
:height (or height 24)}
[react/image {:style (merge (cond-> {:width (or width 24)
:height (or height 24)}
resize-mode
(assoc :resize-mode resize-mode)
resize-mode
(assoc :resize-mode resize-mode)
:always
(assoc :tint-color (match-color color)))
:source (icon-source name)}]]))
:always
(assoc :tint-color (match-color color)))
container-style)
:accessibility-label accessibility-label
:source (icon-source name)}]))
(def icon (memoize memo-icon-fn))
(defn tiny-icon
([name] (tiny-icon name {}))

View File

@ -62,30 +62,41 @@
[react/view {:style (merge style styles/item-checkbox)}
[radio/radio (:checked? props)]])])
(defn- wrap-render-fn [f render-data]
(fn [^js data]
(reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-data])))
(defn- wrap-key-fn [f]
(fn [data index]
{:post [(some? %)]}
(f data index)))
(def memo-wrap-render-fn
(memoize
(fn [f render-data]
(fn [^js data]
(reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-data])))))
(def base-separator [react/view styles/base-separator])
(def default-separator [react/view styles/separator])
(def memo-separator-fn
(memoize
(fn [separator default-separator?]
(reagent/as-element (or separator (when (and platform/ios? default-separator?) default-separator))))))
(def memo-as-element
(memoize
(fn [element]
(reagent/as-element element))))
(def memo-wrap-key-fn
(memoize
(fn [f]
(fn [data index]
{:post [(some? %)]}
(f data index)))))
(defn- base-list-props
[{:keys [key-fn render-fn empty-component header footer separator default-separator? render-data]}]
(let [separator (or separator (when (and platform/ios? default-separator?) default-separator))]
(merge (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))})
;; header and footer not wrapped in anonymous function to prevent re-creation on every re-render
;; More details can be found here - https://github.com/facebook/react-native/issues/13602#issuecomment-300608431
(when header {:ListHeaderComponent (reagent/as-element header)})
(when footer {:ListFooterComponent (reagent/as-element footer)}))))
(merge (when key-fn {:keyExtractor (memo-wrap-key-fn key-fn)})
(when render-fn {:renderItem (memo-wrap-render-fn render-fn render-data)})
(when separator {:ItemSeparatorComponent (memo-separator-fn separator default-separator?)})
(when empty-component {:ListEmptyComponent (memo-as-element empty-component)})
(when header {:ListHeaderComponent (memo-as-element header)})
(when footer {:ListFooterComponent (memo-as-element footer)})))
(defn flat-list
"A wrapper for FlatList.
@ -115,7 +126,7 @@
(defn- wrap-per-section-render-fn [props]
(update
(if-let [f (:render-fn props)]
(assoc (dissoc props :render-fn :render-data) :renderItem (wrap-render-fn f (:render-data props)))
(assoc (dissoc props :render-fn :render-data) :renderItem (memo-wrap-render-fn f (:render-data props)))
props)
:data to-array))

View File

@ -6,15 +6,20 @@
[status-im.multiaccounts.core :as multiaccounts]
[status-im.utils.image :as utils.image]))
(def memo-photo-rend
(memoize
(fn [photo-path size accessibility-label]
(let [identicon? (when photo-path (profile.db/base64-png? photo-path))]
[react/view {:style (style/photo-container size)}
[react/image {:source (utils.image/source photo-path)
:style (style/photo size)
:resize-mode :cover
:accessibility-label (or accessibility-label :chat-icon)}]
(when identicon?
[react/view {:style (style/photo-border size)}])]))))
(defn photo [photo-path {:keys [size accessibility-label]}]
(let [identicon? (when photo-path (profile.db/base64-png? photo-path))]
[react/view {:style (style/photo-container size)}
[react/image {:source (utils.image/source photo-path)
:style (style/photo size)
:resize-mode :cover
:accessibility-label (or accessibility-label :chat-icon)}]
(when identicon?
[react/view {:style (style/photo-border size)}])]))
[memo-photo-rend photo-path size accessibility-label])
;; We optionally pass identicon for perfomance reason, so it does not have to be calculated for each message
(defn member-photo [pub-key identicon]

View File

@ -14,7 +14,7 @@
(re-frame/dispatch [:bottom-sheet/hide])
(re-frame/dispatch event))
(defn one-to-one-chat-accents [{:keys [chat-id]}]
(defn one-to-one-chat-accents [chat-id]
(let [photo @(re-frame/subscribe [:chats/photo-path chat-id])
contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity chat-id])]
[react/view
@ -45,7 +45,7 @@
:icon :main-icons/delete
:on-press #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id])}]]))
(defn public-chat-accents [{:keys [chat-id]}]
(defn public-chat-accents [chat-id]
(let [link (universal-links/generate-link :public-chat :external chat-id)
message (i18n/label :t/share-public-chat-text {:link link})]
[react/view
@ -147,13 +147,13 @@
:icon :main-icons/delete
:on-press #(hide-sheet-and-dispatch [:group-chats.ui/remove-chat-confirmed chat-id])}])]))))
(defn actions [{:keys [chat-type]
(defn actions [{:keys [chat-type chat-id]
:as current-chat}]
(cond
(#{constants/public-chat-type
constants/profile-chat-type
constants/timeline-chat-type} chat-type)
[public-chat-accents current-chat]
[public-chat-accents chat-id]
(= chat-type constants/community-chat-type)
[community-chat-accents current-chat]
@ -161,11 +161,14 @@
(= chat-type constants/private-group-chat-type)
[group-chat-accents current-chat]
:else [one-to-one-chat-accents current-chat]))
:else [one-to-one-chat-accents chat-id]))
(defn current-chat-actions []
[actions @(re-frame/subscribe [:chats/current-chat])])
(defn chat-actions [chat-id]
[actions @(re-frame/subscribe [:chat-by-id chat-id])])
(defn options [chat-id message-id]
(fn []
[react/view

View File

@ -330,38 +330,41 @@
space-keeper (get-space-keeper-ios bottom-space panel-space active-panel text-input-ref)
set-active-panel (get-set-active-panel active-panel)
on-close #(set-active-panel nil)]
(fn []
(let [{:keys [chat-id show-input? group-chat admins invitation-admin] :as chat}
;;we want to react only on these fields, do not use full chat map here
@(re-frame/subscribe [:chats/current-chat-chat-view])
max-bottom-space (max @bottom-space @panel-space)]
[:<>
[topbar]
[connectivity/loading-indicator]
(when chat-id
(if group-chat
[invitation-requests chat-id admins]
[add-contact-bar chat-id]))
;;MESSAGES LIST
[messages-view {:chat chat
:bottom-space max-bottom-space
:pan-responder pan-responder
:space-keeper space-keeper
:show-input? show-input?}]
(when (and group-chat invitation-admin)
[accessory/view {:y position-y
:on-update-inset on-update}
[invitation-bar chat-id]])
[components/autocomplete-mentions text-input-ref max-bottom-space]
(when show-input?
[accessory/view {:y position-y
:pan-state pan-state
:has-panel (boolean @active-panel)
:on-close on-close
:on-update-inset on-update}
[components/chat-toolbar
{:chat-id chat-id
:active-panel @active-panel
:set-active-panel set-active-panel
:text-input-ref text-input-ref}]
[bottom-sheet @active-panel]])]))))
(reagent/create-class
{:component-will-unmount #(re-frame/dispatch-sync [:close-chat])
:reagent-render
(fn []
(let [{:keys [chat-id show-input? group-chat admins invitation-admin] :as chat}
;;we want to react only on these fields, do not use full chat map here
@(re-frame/subscribe [:chats/current-chat-chat-view])
max-bottom-space (max @bottom-space @panel-space)]
[:<>
[topbar]
[connectivity/loading-indicator]
(when chat-id
(if group-chat
[invitation-requests chat-id admins]
[add-contact-bar chat-id]))
;;MESSAGES LIST
[messages-view {:chat chat
:bottom-space max-bottom-space
:pan-responder pan-responder
:space-keeper space-keeper
:show-input? show-input?}]
(when (and group-chat invitation-admin)
[accessory/view {:y position-y
:on-update-inset on-update}
[invitation-bar chat-id]])
[components/autocomplete-mentions text-input-ref max-bottom-space]
(when show-input?
[accessory/view {:y position-y
:pan-state pan-state
:has-panel (boolean @active-panel)
:on-close on-close
:on-update-inset on-update}
[components/chat-toolbar
{:chat-id chat-id
:active-panel @active-panel
:set-active-panel set-active-panel
:text-input-ref text-input-ref}]
[bottom-sheet @active-panel]])]))})))

View File

@ -13,7 +13,8 @@
[status-im.ui.components.colors :as colors]
[status-im.ui.components.toolbar :as toolbar]
[status-im.ui.components.react :as react]
[status-im.ui.screens.communities.icon :as communities.icon]))
[status-im.ui.screens.communities.icon :as communities.icon]
[quo.design-system.colors :as quo.colors]))
(defn hide-sheet-and-dispatch [event]
(>evt [:bottom-sheet/hide])
@ -30,36 +31,35 @@
:accessibility-label :unviewed-messages-public}]))
(defn community-home-list-item [{:keys [id name last?] :as community}]
[react/view
[quo/list-item
{:icon [communities.icon/community-icon community]
:title [react/view {:flex-direction :row
:flex 1}
[react/view {:flex-direction :row
:flex 1
:padding-right 16
:align-items :center}
[quo/text {:weight :medium
:accessibility-label :chat-name-text
:font-size 17
:ellipsize-mode :tail
:number-of-lines 1}
name]]
[react/view {:flex-direction :row
:flex 1
:justify-content :flex-end
:align-items :center}
[community-unviewed-count id]]]
:title-accessibility-label :chat-name-text
:on-press #(do
(>evt [:dismiss-keyboard])
(>evt [:navigate-to :community {:community-id id}]))}]
;; TODO: actions
;; :on-long-press #(>evt [:bottom-sheet/show-sheet
;; nil])
(when last?
[quo/separator])])
[react/touchable-opacity {:style (merge {:height 64}
(when last?
{:border-bottom-color (quo.colors/get-color :ui-01)
:border-bottom-width 1}))
:on-press (fn [id]
(>evt [:dismiss-keyboard])
(>evt [:navigate-to :community {:community-id id}]))}
[:<>
[react/view {:top 12 :left 16 :position :absolute}
[communities.icon/community-icon community]]
[react/view {:style {:margin-left 72
:flex-direction :row
:flex 1}
:accessibility-label :chat-name-text}
[react/view {:flex-direction :row
:flex 1
:padding-right 16
:align-items :center}
[quo/text {:weight :medium
:accessibility-label :chat-name-text
:font-size 17
:ellipsize-mode :tail
:number-of-lines 1}
name]]
[react/view {:flex-direction :row
:flex 1
:justify-content :flex-end
:align-items :center}
[community-unviewed-count id]]]]])
(defn community-list-item [{:keys [id permissions members name description] :as community}]
(let [members-count (count members)
@ -103,13 +103,6 @@
:icon :main-icons/add
:on-press #(hide-sheet-and-dispatch [::communities/open-create-community])}]])
(defn communities-home-list [communities]
[list/flat-list
{:key-fn :id
:keyboard-should-persist-taps :always
:data communities
:render-fn community-home-list-item}])
(defn communities-list [communities]
[list/section-list
{:content-container-style {:padding-vertical 8}

View File

@ -21,7 +21,10 @@
:text-align :right
:letter-spacing 0.4
:align-items :center
:line-height 12})
:line-height 12
:position :absolute
:top 10
:right 16})
(defn chat-tooltip []
{:align-items :center

View File

@ -101,7 +101,7 @@
(defonce search-active? (reagent/atom false))
(defn search-input-wrapper [search-filter chats]
(defn search-input-wrapper [search-filter chats-empty]
[react/view {:padding-horizontal 16
:padding-vertical 10}
[search-input/search-input
@ -109,7 +109,7 @@
:search-filter search-filter
:on-cancel #(re-frame/dispatch [:search/home-filter-changed nil])
:on-blur (fn []
(when-not (seq chats)
(when chats-empty
(re-frame/dispatch [:search/home-filter-changed nil]))
(re-frame/dispatch [::new-chat/clear-new-identity]))
:on-focus (fn [search-filter]
@ -174,12 +174,13 @@
[welcome-blank-page]
[list/flat-list
{:key-fn chat-list-key-fn
:initialNumToRender 5
:keyboard-should-persist-taps :always
:data items
:render-fn render-fn
:header [:<>
(when (or (seq items) @search-active? (seq search-filter))
[search-input-wrapper search-filter items])
[search-input-wrapper search-filter (empty? items)])
[referral-item/list-item]
(when (and (empty? items)
(or @search-active? (seq search-filter)))

View File

@ -12,7 +12,9 @@
[status-im.ui.components.icons.icons :as icons]
[status-im.utils.contenthash :as contenthash]
[status-im.utils.core :as utils]
[status-im.utils.datetime :as time]))
[status-im.utils.datetime :as time]
[status-im.ui.components.chat-icon.styles :as chat-icon.styles]
[status-im.ui.screens.chat.sheets :as sheets]))
(defn mention-element [from]
@(re-frame/subscribe [:contacts/contact-name-by-identity from]))
@ -39,7 +41,7 @@
"mention"
{:components [react/text-class [mention-element literal]]
:length 4} ;; we can't predict name length so take the smallest possible
:length 4} ;; we can't predict name length so take the smallest possible
"status-tag"
(truncate-literal (str "#" literal))
@ -72,7 +74,7 @@
(:components result)))
(defn message-content-text [{:keys [content content-type community-id]}]
[:<>
[react/view {:position :absolute :left 72 :top 32 :right 80}
(cond
(not (and content content-type))
@ -109,76 +111,88 @@
(:text content)
(render-subheader (:parsed-text content)))])
(defn message-timestamp [timestamp]
[react/view
(when timestamp
[react/text {:style styles/datetime-text
:number-of-lines 1
:accessibility-label :last-message-time-text}
;;TODO (perf) move to event
(string/upper-case (time/to-short-str timestamp))])])
(def memo-timestamp
(memoize
(fn [timestamp]
(string/upper-case (time/to-short-str timestamp)))))
(defn unviewed-indicator [{:keys [unviewed-messages-count public?]}]
(when (pos? unviewed-messages-count)
[react/view {:padding-left 16
:justify-content :flex-end
:align-items :flex-end}
[react/view {:position :absolute :right 16 :bottom 12}
(if public?
[react/view {:style styles/public-unread
:accessibility-label :unviewed-messages-public}]
[badge/message-counter unviewed-messages-count])]))
(def memo-on-long-press
(memoize
(fn [chat-id]
(fn []
(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (fn [] [sheets/chat-actions chat-id])}])))))
(def memo-on-press
(memoize
(fn [chat-id]
(fn []
(re-frame/dispatch [:dismiss-keyboard])
(re-frame/dispatch [:chat.ui/navigate-to-chat chat-id])
(re-frame/dispatch [:search/home-filter-changed nil])))))
(defn icon-style []
{:color colors/black
:width 15
:height 15
:container-style {:width 15
:height 15
:margin-right 2}})
:container-style {:top 13 :left 72
:position :absolute
:width 15
:height 15
:margin-right 2}})
(defn chat-item-icon [muted private-group? public-group?]
(cond
muted
[icons/icon :main-icons/tiny-muted (assoc (icon-style) :color colors/gray)]
private-group?
[icons/icon :main-icons/tiny-group (icon-style)]
public-group?
[icons/icon :main-icons/tiny-public (icon-style)]
:else
[icons/icon :main-icons/tiny-new-contact (icon-style)]))
(defn chat-item-title [chat-id muted group-chat chat-name]
[quo/text {:weight :medium
:color (when muted :secondary)
:accessibility-label :chat-name-text
:ellipsize-mode :tail
:number-of-lines 1
:style {:position :absolute :left 92 :top 10 :right 90}}
(if group-chat
(utils/truncate-str chat-name 30)
;; This looks a bit odd, but I would like only to subscribe
;; if it's a one-to-one. If wrapped in a component styling
;; won't be applied correctly.
(first @(re-frame/subscribe [:contacts/contact-two-names-by-identity chat-id])))])
(defn home-list-item [home-item opts]
(let [{:keys [chat-id chat-name color online group-chat
public? timestamp last-message muted]}
home-item
private-group? (and group-chat (not public?))
public-group? (and group-chat public?)]
[quo/list-item
(merge {:icon [chat-icon.screen/chat-icon-view-chat-list
chat-id group-chat chat-name color online false]
:title [react/view {:flex-direction :row
:flex 1}
[react/view {:flex-direction :row
:flex 1
:padding-right 16
:align-items :center}
(cond
muted
[icons/icon :main-icons/tiny-muted (assoc (icon-style) :color colors/gray)]
private-group?
[icons/icon :main-icons/tiny-group (icon-style)]
public-group?
[icons/icon :main-icons/tiny-public (icon-style)]
:else
[icons/icon :main-icons/tiny-new-contact (icon-style)])
[quo/text {:weight :medium
:color (when muted :secondary)
:accessibility-label :chat-name-text
:ellipsize-mode :tail
:number-of-lines 1}
(if group-chat
(utils/truncate-str chat-name 30)
;; This looks a bit odd, but I would like only to subscribe
;; if it's a one-to-one. If wrapped in a component styling
;; won't be applied correctly.
(first @(re-frame/subscribe [:contacts/contact-two-names-by-identity chat-id])))]]
[message-timestamp (if (pos? (:whisper-timestamp last-message))
(:whisper-timestamp last-message)
timestamp)]]
:title-accessibility-label :chat-name-text
:subtitle [react/view {:flex-direction :row}
[react/view {:flex 1}
[message-content-text (select-keys last-message [:content
:content-type
:community-id])]]
[unviewed-indicator home-item]]}
opts)]))
(let [{:keys [chat-id chat-name color group-chat public? timestamp last-message muted]} home-item]
[react/touchable-opacity (merge {:style {:height 64}} opts)
[:<>
[chat-item-icon muted (and group-chat (not public?)) (and group-chat public?)]
[chat-icon.screen/chat-icon-view chat-id group-chat chat-name
{:container (assoc chat-icon.styles/container-chat-list
:top 12 :left 16 :position :absolute)
:size 40
:chat-icon chat-icon.styles/chat-icon-chat-list
:default-chat-icon (chat-icon.styles/default-chat-icon-chat-list color)
:default-chat-icon-text (chat-icon.styles/default-chat-icon-text 40)}]
[chat-item-title chat-id muted group-chat chat-name]
[react/text {:style styles/datetime-text
:number-of-lines 1
:accessibility-label :last-message-time-text}
;;TODO (perf) move to event
(memo-timestamp (if (pos? (:whisper-timestamp last-message))
(:whisper-timestamp last-message)
timestamp))]
[message-content-text (select-keys last-message [:content :content-type :community-id])]
[unviewed-indicator home-item]]]))

View File

@ -2,7 +2,7 @@
(:require [clojure.string :as string]
#?(:cljs [taoensso.timbre :as log])))
(defn truncate-str
(defn truncate-str-memo
"Given string and max threshold, trims the string to threshold length with `...`
appended to end or in the middle if length of the string exceeds max threshold,
returns the same string if threshold is not exceeded"
@ -19,6 +19,8 @@
(str (subs s 0 (- threshold 3)) "..."))
s))
(def truncate-str (memoize truncate-str-memo))
(defn clean-text [s]
(-> s
(string/replace #"\n" "")