new searchbar design

Signed-off-by: Volodymyr Kozieiev <vkjr.sp@gmail.com>
This commit is contained in:
Volodymyr Kozieiev 2020-01-20 11:52:07 +02:00
parent fb9ea6529f
commit f3df8b2ec8
No known key found for this signature in database
GPG Key ID: 82B04968DF4C0535
8 changed files with 136 additions and 208 deletions

View File

@ -441,6 +441,7 @@ var TopLevel = {
"scrollTo" : function () {},
"scrollToEnd" : function () {},
"scrollToIndex" : function () {},
"scrollToLocation" : function () {},
"section" : function () {},
"selection" : function () {},
"sendDirectMessage" : function () {},

View File

@ -1784,15 +1784,21 @@
(let [{:keys [name random-name tags]} (val chat)]
(into [name random-name] tags)))
(defn sort-by-timestamp
[coll]
(when (not-empty coll)
(sort-by #(-> % second :timestamp) >
(into {} coll))))
(defn apply-filter
"extract-attributes-fn is a function that take an element from the collection
and returns a vector of attributes which are strings
apply-filter returns the elements for which at least one attribute includes
the search-filter
apply-filter returns nil if the search-filter is empty or if there is no element
that match the filter"
apply-filter returns nil if there is no element that match the filter
apply-filter returns full collection if the search-filter is empty"
[search-filter coll extract-attributes-fn]
(when (not-empty search-filter)
(if (not-empty search-filter)
(let [search-filter (string/lower-case search-filter)
results (filter (fn [element]
(some (fn [s]
@ -1801,9 +1807,8 @@
search-filter)))
(extract-attributes-fn element)))
coll)]
(when (not-empty results)
(sort-by #(-> % second :timestamp) >
(into {} results))))))
(sort-by-timestamp results))
(sort-by-timestamp coll)))
(re-frame/reg-sub
:search/filtered-chats

View File

@ -190,12 +190,14 @@
(defn- base-list-props
[{:keys [key-fn render-fn empty-component header footer separator default-separator?]}]
(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)})
(when separator {:ItemSeparatorComponent (fn [] (reagent/as-element separator))})
(when empty-component {:ListEmptyComponent (fn [] (reagent/as-element empty-component))})
(when header {:ListHeaderComponent (fn [] (reagent/as-element header))})
(when footer {:ListFooterComponent (fn [] (reagent/as-element footer))}))))
(merge (when key-fn {:keyExtractor (wrap-key-fn key-fn)})
(when render-fn {:renderItem (wrap-render-fn render-fn)})
(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)}))))
;; Workaround an issue in reagent that does not consider JS array as JS value
;; This forces clj <-> js serialization and breaks clj semantic

View File

@ -1,140 +0,0 @@
(ns status-im.ui.screens.home.filter.views
(:require [status-im.ui.components.list.views :as list]
[status-im.ui.screens.home.styles :as styles]
[status-im.ui.components.react :as react]
[status-im.i18n :as i18n]
[status-im.ui.components.colors :as colors]
[status-im.ui.screens.home.views.inner-item :as inner-item]
[status-im.utils.utils :as utils]
[status-im.ui.components.animation :as animation]
[reagent.core :as reagent]
[re-frame.core :as re-frame]
[status-im.ui.components.icons.vector-icons :as icons]))
(def animation-duration 150)
(defn search-input [_ {:keys [on-cancel on-focus on-change]}]
(let [input-is-focused? (reagent/atom false)
input-ref (reagent/atom nil)]
(fn [search-filter]
(let [show-cancel? (or @input-is-focused?
search-filter)]
[react/view {:style styles/search-container}
[react/view {:style styles/search-input-container}
[icons/icon :main-icons/search {:color colors/gray
:container-style {:margin-left 6
:margin-right 2}}]
[react/text-input {:placeholder (i18n/label :t/search)
:blur-on-submit true
:multiline false
:ref #(reset! input-ref %)
:style styles/search-input
:default-value search-filter
:on-focus #(do
(when on-focus
(on-focus search-filter))
(reset! input-is-focused? true))
:on-change (fn [e]
(let [native-event (.-nativeEvent e)
text (.-text native-event)]
(when on-change
(on-change text))))}]]
(when show-cancel?
[react/touchable-highlight
{:on-press #(do
(when on-cancel
(on-cancel))
(.blur @input-ref)
(reset! input-is-focused? false))
:style {:margin-left 16}}
[react/text {:style {:color colors/blue}}
(i18n/label :t/cancel)]])]))))
(defonce search-input-state
(reagent/atom {:show? false
:height (animation/create-value
(- styles/search-input-height))
:to-hide? false}))
(defn show-search!
[]
(when-not (:to-hide? @search-input-state)
(swap! search-input-state assoc :show? true)
(animation/start
(animation/timing (:height @search-input-state)
{:toValue 0
:duration animation-duration
:easing (.out (animation/easing)
(.-quad (animation/easing)))
:useNativeDriver true})
#(swap! search-input-state assoc :to-hide? true))))
(defn update-search-state!
[]
(let [visible? (:to-hide? @search-input-state)]
(swap! search-input-state assoc :show? visible?)
(animation/set-value (:height @search-input-state)
(if visible? 0 (- styles/search-input-height)))))
(defn hide-search!
[]
(utils/set-timeout
#(swap! search-input-state assoc :show? false)
350)
(animation/start
(animation/timing (:height @search-input-state)
{:toValue (- styles/search-input-height)
:duration animation-duration
:easing (.in (animation/easing)
(.-quad (animation/easing)))
:useNativeDriver true})
#(swap! search-input-state assoc :to-hide? false)))
(defn search-input-wrapper
[search-filter]
(reagent/create-class
{:component-will-unmount
#(update-search-state!)
:component-did-mount
#(update-search-state!)
:reagent-render
(fn [search-filter]
[search-input search-filter
{:on-cancel #(do
(re-frame/dispatch [:search/filter-changed nil])
(hide-search!))
:on-focus (fn [search-filter]
(when-not search-filter
(re-frame/dispatch [:search/filter-changed ""])))
:on-change (fn [text]
(re-frame/dispatch [:search/filter-changed text]))}])}))
(defn home-filtered-items-list
[chats]
[list/section-list
{:style {:margin-bottom -35}
:sections [{:title :t/chats
:data chats}]
:key-fn first
;; true by default on iOS
:stickySectionHeadersEnabled false
:render-section-header-fn
(fn [{:keys [title data]}]
[react/view {:style {:height 40}}
[react/text {:style styles/filter-section-title}
(i18n/label title)]])
:render-section-footer-fn
(fn [{:keys [title data]}]
(when (empty? data)
[list/big-list-item
{:text (i18n/label :t/no-result)
:text-color colors/gray
:hide-chevron? true
:action-fn #()
:icon (case title
"messages" :main-icons/one-on-one-chat
"browser" :main-icons/browser
"chats" :main-icons/message)
:icon-color colors/gray}]))
:render-fn (fn [home-item]
[inner-item/home-list-item home-item])}])

View File

@ -1,6 +1,7 @@
(ns status-im.ui.screens.home.styles
(:require [status-im.ui.components.colors :as colors]
[status-im.utils.styles :as styles]))
[status-im.utils.styles :as styles]
[status-im.utils.platform :as platform]))
(defn toolbar []
{:background-color colors/white})
@ -24,12 +25,17 @@
(def search-input-height 56)
(def search-container
{:height search-input-height
:flex-direction :row
:padding-horizontal 16
:background-color colors/white
:align-items :center
:justify-content :center})
(merge
{:height search-input-height
:flex-direction :row
:padding-horizontal 16
:background-color colors/white
:align-items :center
:justify-content :center}
(when platform/ios?
{:position :absolute
:top (- search-input-height)
:width "100%"})))
(def search-input-container
{:background-color colors/gray-lighter

View File

@ -10,7 +10,6 @@
[status-im.ui.components.react :as react]
[status-im.ui.components.toolbar.view :as toolbar]
[status-im.ui.screens.home.styles :as styles]
[status-im.ui.screens.home.filter.views :as filter.views]
[status-im.utils.platform :as platform]
[status-im.ui.components.tabbar.styles :as tabs.styles]
[status-im.ui.screens.home.views.inner-item :as inner-item]
@ -23,6 +22,8 @@
[status-im.ui.components.button :as button])
(:require-macros [status-im.utils.views :as views]))
(defonce search-active? (reagent/atom false))
(defn welcome-video-wrapper []
[react/view {:style {:align-items :center
:justify-content :center
@ -85,45 +86,101 @@
[react/view {:style {:flex 1 :flex-direction :row :align-items :center :justify-content :center}}
[react/i18n-text {:style styles/welcome-blank-text :key :welcome-blank-message}]])
(defn home-items-view [_ _ _ _ search-input-state]
(let [analyze-pos? (reagent/atom true)
start-pos (reagent/atom 0)]
(fn [search-filter chats all-home-items hide-home-tooltip?]
(if search-filter
[filter.views/home-filtered-items-list chats]
[react/animated-view
{:style {:flex 1
:background-color :white
:margin-bottom (- styles/search-input-height)
:transform [{:translateY (:height @search-input-state)}]}}
(if (or (seq all-home-items) (not hide-home-tooltip?))
[list/flat-list (merge {:data all-home-items
:key-fn first
:header [react/view {:height 4 :flex 1}]
:footer [react/view
(when-not hide-home-tooltip?
[home-tooltip-view])
[react/view {:height 68 :flex 1}]]
:on-scroll-begin-drag
(fn [e]
(reset! analyze-pos? true)
(reset! start-pos (.-y (.-contentOffset (.-nativeEvent e)))))
:on-scroll-end-drag
(fn [e]
(reset! analyze-pos? false))
:on-scroll
(fn [e]
(let [y-pos (.-y (.-contentOffset (.-nativeEvent e)))
scroling-down? (> y-pos @start-pos)
scrolling-up-from-top? (< y-pos -20)]
(if (and @analyze-pos? scrolling-up-from-top?)
(filter.views/show-search!))
(if (and @analyze-pos? scroling-down?)
(filter.views/hide-search!))))
:render-fn
(fn [home-item _]
[inner-item/home-list-item home-item])})]
[welcome-blank-page])]))))
(defn chat-list-footer [hide-home-tooltip?]
(let [show-tooltip? (and (not hide-home-tooltip?) (not @search-active?))]
[react/view
(when show-tooltip?
[home-tooltip-view])
[react/view {:height 68 :flex 1}]]))
(views/defview search-input [{:keys [on-cancel on-focus on-change]}]
(views/letsubs
[{:keys [search-filter]} [:home-items]
input-ref (reagent/atom nil)]
[react/view {:style styles/search-container}
[react/view {:style styles/search-input-container}
[icons/icon :main-icons/search {:color colors/gray
:container-style {:margin-left 6
:margin-right 2}}]
[react/text-input {:placeholder (i18n/label :t/search)
:blur-on-submit true
:multiline false
:ref #(reset! input-ref %)
:style styles/search-input
:default-value search-filter
:on-focus #(do
(when on-focus
(on-focus search-filter))
(reset! search-active? true))
:on-change (fn [e]
(let [native-event (.-nativeEvent e)
text (.-text native-event)]
(when on-change
(on-change text))))}]]
(when @search-active?
[react/touchable-highlight
{:on-press (fn []
(.clear @input-ref)
(.blur @input-ref)
(when on-cancel
(on-cancel))
(reset! search-active? false))
:style {:margin-left 16}}
[react/text {:style {:color colors/blue}}
(i18n/label :t/cancel)]])]))
(defn search-input-wrapper []
[search-input
{:on-cancel #(re-frame/dispatch [:search/filter-changed nil])
:on-focus (fn [search-filter]
(when-not search-filter
(re-frame/dispatch [:search/filter-changed ""])))
:on-change (fn [text]
(re-frame/dispatch [:search/filter-changed text]))}])
(defn section-footer [{:keys [title data]}]
(when (and @search-active? (empty? data))
[list/big-list-item
{:text (i18n/label :t/no-result)
:text-color colors/gray
:hide-chevron? true
:action-fn #()
:icon (case title
"messages" :main-icons/one-on-one-chat
"browser" :main-icons/browser
"chats" :main-icons/message)
:icon-color colors/gray}]))
(views/defview home-filtered-items-list []
(views/letsubs
[{:keys [chats all-home-items]} [:home-items]
{:keys [hide-home-tooltip?]} [:multiaccount]]
(let [list-ref (reagent/atom nil)]
[list/section-list
(merge
{:sections [{:title :t/chats
:data (if @search-active? chats all-home-items)}]
:key-fn first
;; true by default on iOS
:stickySectionHeadersEnabled false
:keyboard-should-persist-taps :always
:ref #(reset! list-ref %)
:footer [chat-list-footer hide-home-tooltip?]
:contentInset {:top styles/search-input-height}
:render-section-header-fn (fn [data] [react/view])
:render-section-footer-fn section-footer
:render-fn (fn [home-item]
[inner-item/home-list-item home-item])
:header (when (or @search-active? (not-empty all-home-items))
[search-input-wrapper])
:on-scroll-end-drag
(fn [e]
(let [y (-> e .-nativeEvent .-contentOffset .-y)
hide-searchbar? (cond
platform/ios? (and (neg? y) (> y (- (/ styles/search-input-height 2))))
platform/android? (and (< y styles/search-input-height) (> y (/ styles/search-input-height 2))))]
(if hide-searchbar?
(.scrollToLocation @list-ref #js {:sectionIndex 0 :itemIndex 0}))))})])))
(views/defview home-action-button [home-width]
(views/letsubs [logging-in? [:multiaccounts/login]]
@ -139,7 +196,7 @@
(views/defview home [loading?]
(views/letsubs
[anim-translate-y (animation/create-value connectivity/neg-connectivity-bar-height)
{:keys [search-filter chats all-home-items]} [:home-items]
{:keys [all-home-items]} [:home-items]
{:keys [hide-home-tooltip?]} [:multiaccount]
window-width [:dimensions/window-width]
two-pane-ui-enabled? [:two-pane-ui-enabled?]]
@ -172,13 +229,9 @@
[react/activity-indicator {:flex 1
:animating true}]
[react/view {:flex 1}
[filter.views/search-input-wrapper search-filter]
[home-items-view
search-filter
chats
all-home-items
hide-home-tooltip?
filter.views/search-input-state]])]
(if (and (empty? all-home-items) hide-home-tooltip? (not @search-active?))
[welcome-blank-page]
[home-filtered-items-list])])]
[home-action-button home-width]]])))
(views/defview home-wrapper []

View File

@ -83,6 +83,7 @@
tribute-label))
:subtitle-row-accessory [unviewed-indicator chat-id]
:on-press #(do
(re-frame/dispatch [:dismiss-keyboard])
(re-frame/dispatch [:chat.ui/navigate-to-chat chat-id])
(re-frame/dispatch [:chat.ui/mark-messages-seen :chat]))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet chat-actions {:chat-id chat-id}])}]))

View File

@ -16,7 +16,7 @@
:random-name "random-name4"
:tags #{"tag4"}}}]
(testing "no search filter"
(is (= 0
(is (= (count chats)
(count (search.subs/apply-filter ""
chats
search.subs/extract-chat-attributes)))))