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

View File

@ -10,10 +10,9 @@
;;TODO REWORK THIS NAMESPACE ;;TODO REWORK THIS NAMESPACE
(defn default-chat-icon [name styles] (def get-name-first-char
(when-not (string/blank? name) (memoize
[react/view (:default-chat-icon styles) (fn [name]
[react/text {:style (:default-chat-icon-text styles)}
;; TODO: for now we check if the first letter is a # ;; TODO: for now we check if the first letter is a #
;; which means it is most likely a public chat and ;; which means it is most likely a public chat and
;; use the second letter if that is the case ;; use the second letter if that is the case
@ -22,7 +21,13 @@
(string/capitalize (if (and (= "#" (first name)) (string/capitalize (if (and (= "#" (first name))
(< 1 (count name))) (< 1 (count name)))
(second name) (second name)
(first 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)}
(get-name-first-char name)]]))
(defn chat-icon-view (defn chat-icon-view
[chat-id group-chat name styles] [chat-id group-chat name styles]

View File

@ -26,19 +26,13 @@
:else :else
colors/black)) colors/black))
(defn icon (defn memo-icon-fn
([name] (icon name nil)) ([name] (memo-icon-fn name nil))
([name {:keys [color resize-mode container-style ([name {:keys [color resize-mode container-style
accessibility-label width height] accessibility-label width height]
:or {accessibility-label :icon}}] :or {accessibility-label :icon}}]
^{:key name} ^{:key name}
[react/view [react/image {:style (merge (cond-> {:width (or width 24)
{: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)} :height (or height 24)}
resize-mode resize-mode
@ -46,7 +40,11 @@
:always :always
(assoc :tint-color (match-color color))) (assoc :tint-color (match-color color)))
:source (icon-source name)}]])) container-style)
:accessibility-label accessibility-label
:source (icon-source name)}]))
(def icon (memoize memo-icon-fn))
(defn tiny-icon (defn tiny-icon
([name] (tiny-icon name {})) ([name] (tiny-icon name {}))

View File

@ -62,30 +62,41 @@
[react/view {:style (merge style styles/item-checkbox)} [react/view {:style (merge style styles/item-checkbox)}
[radio/radio (:checked? props)]])]) [radio/radio (:checked? props)]])])
(defn- wrap-render-fn [f render-data] (def memo-wrap-render-fn
(memoize
(fn [f render-data]
(fn [^js data] (fn [^js data]
(reagent/as-element [f (.-item data) (.-index data) (.-separators data) render-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 base-separator [react/view styles/base-separator]) (def base-separator [react/view styles/base-separator])
(def default-separator [react/view styles/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 (defn- base-list-props
[{:keys [key-fn render-fn empty-component header footer separator default-separator? render-data]}] [{: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 (memo-wrap-key-fn key-fn)})
(merge (when key-fn {:keyExtractor (wrap-key-fn key-fn)}) (when render-fn {:renderItem (memo-wrap-render-fn render-fn render-data)})
(when render-fn {:renderItem (wrap-render-fn render-fn render-data)}) (when separator {:ItemSeparatorComponent (memo-separator-fn separator default-separator?)})
(when separator {:ItemSeparatorComponent (fn [] (reagent/as-element separator))}) (when empty-component {:ListEmptyComponent (memo-as-element empty-component)})
(when empty-component {:ListEmptyComponent (fn [] (reagent/as-element empty-component))}) (when header {:ListHeaderComponent (memo-as-element header)})
;; header and footer not wrapped in anonymous function to prevent re-creation on every re-render (when footer {:ListFooterComponent (memo-as-element footer)})))
;; 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)}))))
(defn flat-list (defn flat-list
"A wrapper for FlatList. "A wrapper for FlatList.
@ -115,7 +126,7 @@
(defn- wrap-per-section-render-fn [props] (defn- wrap-per-section-render-fn [props]
(update (update
(if-let [f (:render-fn props)] (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) props)
:data to-array)) :data to-array))

View File

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

View File

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

View File

@ -330,6 +330,9 @@
space-keeper (get-space-keeper-ios bottom-space panel-space active-panel text-input-ref) space-keeper (get-space-keeper-ios bottom-space panel-space active-panel text-input-ref)
set-active-panel (get-set-active-panel active-panel) set-active-panel (get-set-active-panel active-panel)
on-close #(set-active-panel nil)] on-close #(set-active-panel nil)]
(reagent/create-class
{:component-will-unmount #(re-frame/dispatch-sync [:close-chat])
:reagent-render
(fn [] (fn []
(let [{:keys [chat-id show-input? group-chat admins invitation-admin] :as chat} (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 ;;we want to react only on these fields, do not use full chat map here
@ -364,4 +367,4 @@
:active-panel @active-panel :active-panel @active-panel
:set-active-panel set-active-panel :set-active-panel set-active-panel
:text-input-ref text-input-ref}] :text-input-ref text-input-ref}]
[bottom-sheet @active-panel]])])))) [bottom-sheet @active-panel]])]))})))

View File

@ -13,7 +13,8 @@
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.toolbar :as toolbar] [status-im.ui.components.toolbar :as toolbar]
[status-im.ui.components.react :as react] [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] (defn hide-sheet-and-dispatch [event]
(>evt [:bottom-sheet/hide]) (>evt [:bottom-sheet/hide])
@ -30,11 +31,20 @@
:accessibility-label :unviewed-messages-public}])) :accessibility-label :unviewed-messages-public}]))
(defn community-home-list-item [{:keys [id name last?] :as community}] (defn community-home-list-item [{:keys [id name last?] :as community}]
[react/view [react/touchable-opacity {:style (merge {:height 64}
[quo/list-item (when last?
{:icon [communities.icon/community-icon community] {:border-bottom-color (quo.colors/get-color :ui-01)
:title [react/view {:flex-direction :row :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} :flex 1}
:accessibility-label :chat-name-text}
[react/view {:flex-direction :row [react/view {:flex-direction :row
:flex 1 :flex 1
:padding-right 16 :padding-right 16
@ -49,17 +59,7 @@
:flex 1 :flex 1
:justify-content :flex-end :justify-content :flex-end
:align-items :center} :align-items :center}
[community-unviewed-count id]]] [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])])
(defn community-list-item [{:keys [id permissions members name description] :as community}] (defn community-list-item [{:keys [id permissions members name description] :as community}]
(let [members-count (count members) (let [members-count (count members)
@ -103,13 +103,6 @@
:icon :main-icons/add :icon :main-icons/add
:on-press #(hide-sheet-and-dispatch [::communities/open-create-community])}]]) :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] (defn communities-list [communities]
[list/section-list [list/section-list
{:content-container-style {:padding-vertical 8} {:content-container-style {:padding-vertical 8}

View File

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

View File

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

View File

@ -12,7 +12,9 @@
[status-im.ui.components.icons.icons :as icons] [status-im.ui.components.icons.icons :as icons]
[status-im.utils.contenthash :as contenthash] [status-im.utils.contenthash :as contenthash]
[status-im.utils.core :as utils] [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] (defn mention-element [from]
@(re-frame/subscribe [:contacts/contact-name-by-identity from])) @(re-frame/subscribe [:contacts/contact-name-by-identity from]))
@ -72,7 +74,7 @@
(:components result))) (:components result)))
(defn message-content-text [{:keys [content content-type community-id]}] (defn message-content-text [{:keys [content content-type community-id]}]
[:<> [react/view {:position :absolute :left 72 :top 32 :right 80}
(cond (cond
(not (and content content-type)) (not (and content content-type))
@ -109,48 +111,45 @@
(:text content) (:text content)
(render-subheader (:parsed-text content)))]) (render-subheader (:parsed-text content)))])
(defn message-timestamp [timestamp] (def memo-timestamp
[react/view (memoize
(when timestamp (fn [timestamp]
[react/text {:style styles/datetime-text (string/upper-case (time/to-short-str timestamp)))))
:number-of-lines 1
:accessibility-label :last-message-time-text}
;;TODO (perf) move to event
(string/upper-case (time/to-short-str timestamp))])])
(defn unviewed-indicator [{:keys [unviewed-messages-count public?]}] (defn unviewed-indicator [{:keys [unviewed-messages-count public?]}]
(when (pos? unviewed-messages-count) (when (pos? unviewed-messages-count)
[react/view {:padding-left 16 [react/view {:position :absolute :right 16 :bottom 12}
:justify-content :flex-end
:align-items :flex-end}
(if public? (if public?
[react/view {:style styles/public-unread [react/view {:style styles/public-unread
:accessibility-label :unviewed-messages-public}] :accessibility-label :unviewed-messages-public}]
[badge/message-counter unviewed-messages-count])])) [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 [] (defn icon-style []
{:color colors/black {:color colors/black
:width 15 :width 15
:height 15 :height 15
:container-style {:width 15 :container-style {:top 13 :left 72
:position :absolute
:width 15
:height 15 :height 15
:margin-right 2}}) :margin-right 2}})
(defn home-list-item [home-item opts] (defn chat-item-icon [muted private-group? public-group?]
(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 (cond
muted muted
[icons/icon :main-icons/tiny-muted (assoc (icon-style) :color colors/gray)] [icons/icon :main-icons/tiny-muted (assoc (icon-style) :color colors/gray)]
@ -159,26 +158,41 @@
public-group? public-group?
[icons/icon :main-icons/tiny-public (icon-style)] [icons/icon :main-icons/tiny-public (icon-style)]
:else :else
[icons/icon :main-icons/tiny-new-contact (icon-style)]) [icons/icon :main-icons/tiny-new-contact (icon-style)]))
(defn chat-item-title [chat-id muted group-chat chat-name]
[quo/text {:weight :medium [quo/text {:weight :medium
:color (when muted :secondary) :color (when muted :secondary)
:accessibility-label :chat-name-text :accessibility-label :chat-name-text
:ellipsize-mode :tail :ellipsize-mode :tail
:number-of-lines 1} :number-of-lines 1
:style {:position :absolute :left 92 :top 10 :right 90}}
(if group-chat (if group-chat
(utils/truncate-str chat-name 30) (utils/truncate-str chat-name 30)
;; This looks a bit odd, but I would like only to subscribe ;; 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 ;; if it's a one-to-one. If wrapped in a component styling
;; won't be applied correctly. ;; won't be applied correctly.
(first @(re-frame/subscribe [:contacts/contact-two-names-by-identity chat-id])))]] (first @(re-frame/subscribe [:contacts/contact-two-names-by-identity chat-id])))])
[message-timestamp (if (pos? (:whisper-timestamp last-message))
(defn home-list-item [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) (:whisper-timestamp last-message)
timestamp)]] timestamp))]
:title-accessibility-label :chat-name-text [message-content-text (select-keys last-message [:content :content-type :community-id])]
:subtitle [react/view {:flex-direction :row} [unviewed-indicator home-item]]]))
[react/view {:flex 1}
[message-content-text (select-keys last-message [:content
:content-type
:community-id])]]
[unviewed-indicator home-item]]}
opts)]))

View File

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