[feature] search chats

Signed-off-by: Igor Mandrigin <i@mandrigin.ru>
This commit is contained in:
yenda 2019-02-07 11:18:00 +01:00 committed by Igor Mandrigin
parent 4f89073ae8
commit 6b6847a1ba
No known key found for this signature in database
GPG Key ID: 4A0EDDE26E66BC8B
9 changed files with 334 additions and 70 deletions

View File

@ -5,30 +5,48 @@
(re-frame/reg-sub
:search/filter
(fn [db]
(get-in db [:ui/search :filter] "")))
(get-in db [:ui/search :filter])))
(defn filter-chats
[[chats search-filter]]
(if (empty? search-filter)
chats
(let [search-filter (string/lower-case search-filter)]
(keep #(let [{:keys [name random-name tags]} (val %)]
(when (some (fn [s]
(when s
(string/includes? (string/lower-case s)
search-filter)))
(into [name random-name] tags))
%))
chats))))
(defn extract-browser-attributes
[browser]
(let [{:keys [browser-id name]} (val browser)]
[browser-id name]))
(defn extract-chat-attributes [chat]
(let [{:keys [name random-name tags]} (val chat)]
(into [name random-name] tags)))
(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"
[search-filter coll extract-attributes-fn]
(when (not-empty search-filter)
(let [search-filter (string/lower-case search-filter)
results (filter (fn [element]
(some (fn [s]
(when (string? s)
(string/includes? (string/lower-case s)
search-filter)))
(extract-attributes-fn element)))
coll)]
(when (not-empty results)
(sort-by #(-> % second :timestamp) >
(into {} results))))))
(re-frame/reg-sub
:search/filtered-active-chats
:search/filtered-chats
:<- [:chats/active-chats]
:<- [:search/filter]
filter-chats)
(fn [[chats search-filter]]
(apply-filter search-filter chats extract-chat-attributes)))
(re-frame/reg-sub
:search/filtered-home-items
:<- [:search/filtered-active-chats]
(fn [active-chats]
(sort-by #(-> % second :timestamp) > active-chats)))
:search/filtered-browsers
:<- [:browser/browsers]
:<- [:search/filter]
(fn [[browsers search-filter]]
(apply-filter search-filter browsers extract-browser-attributes)))

View File

@ -83,7 +83,7 @@
:flex-direction :row
:align-items :center
:background-color colors/white
:height 52})
:height 64})
(defn settings-item-icon [icon-color]
{:background-color (colors/alpha icon-color 0.1)

View File

@ -12,7 +12,7 @@
[flat-list {:data [{:title \"\" :subtitle \"\"}] :render-fn render}]
[section-list {:sections [{:title :key :unik :data {:title \"\" :subtitle \"\"}}] :render-fn render}]
[section-list {:sections [{:title \"\" :key :unik :data {:title \"\" :subtitle \"\"}}] :render-fn render}]
or with a per-section `render-fn`
@ -118,9 +118,9 @@
(and action-fn text)
(or (nil? accessibility-label) (keyword? accessibility-label))]}
[react/touchable-highlight
(cond-> {:on-press action-fn
:accessibility-label accessibility-label
:disabled (not active?)})
{:on-press action-fn
:accessibility-label accessibility-label
:disabled (not active?)}
[react/view styles/settings-item
(if icon
[react/view (styles/settings-item-icon icon-color)
@ -218,12 +218,16 @@
(defn section-list
"A wrapper for SectionList.
To render something on empty sections, use renderSectionFooter and conditionaly
render on empty data
See https://facebook.github.io/react-native/docs/sectionlist.html"
[{:keys [sections render-section-header-fn] :as props
[{:keys [sections render-section-header-fn render-section-footer-fn] :as props
:or {render-section-header-fn default-render-section-header}}]
[section-list-class
(merge (base-list-props props)
props
(when render-section-footer-fn
{:renderSectionFooter (wrap-render-section-header-fn render-section-footer-fn)})
{:sections (clj->js (map wrap-per-section-render-fn sections))
:renderSectionHeader (wrap-render-section-header-fn render-section-header-fn)})])

View File

@ -146,7 +146,7 @@
(views/defview chat-list-view [loading?]
(views/letsubs [search-filter [:search/filter]
logging-in? [:get :accounts/login]
filtered-home-items [:search/filtered-home-items]]
{:keys [all-home-items chats]} [:home-items]]
{:component-did-mount
(fn [this]
(let [[_ loading?] (.. this -props -argv)]
@ -163,7 +163,7 @@
[icons/icon :main-icons/add {:style {:tint-color :white}}])]]]]
[react/scroll-view {:enableArrayScrollingOptimization true}
[react/view
(for [[index chat] (map-indexed vector filtered-home-items)]
(for [[index chat] (map-indexed vector (or all-home-items chats))]
^{:key (first chat)}
[chat-list-item chat])]]]))

View File

@ -1,6 +1,7 @@
(ns status-im.ui.screens.home.styles
(:require-macros [status-im.utils.styles :refer [defstyle defnstyle]])
(:require [status-im.ui.components.colors :as colors]))
(:require [status-im.ui.components.colors :as colors]
[status-im.utils.platform :as platform]))
(defn toolbar []
{:background-color colors/white})
@ -94,6 +95,40 @@
:ios {:font-size 15
:height 24}})
(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})
(def search-input-container
{:background-color colors/gray-lighter
:flex 1
:flex-direction :row
:height 36
:align-items :center
:justify-content :center
:border-radius 8})
(def search-input
(merge {:flex 1
:font-size 15}
(when platform/android?
{:line-height 22
:margin 0
:padding 0})))
(def filter-section-title
{:font-size 15
:margin-left 16
:margin-top 14
:margin-bottom 4
:color colors/gray})
(def status-container
{:flex-direction :row
:top 16

View File

@ -1,12 +1,23 @@
(ns status-im.ui.screens.home.subs
(:require [re-frame.core :as re-frame]))
(:require [re-frame.core :as re-frame]
[status-im.utils.platform :as platform]))
(re-frame/reg-sub
:home-items
:<- [:chats/active-chats]
:<- [:browser/browsers]
(fn [[chats browsers]]
(sort-by #(-> % second :timestamp) > (merge chats browsers))))
:<- [:search/filter]
:<- [:search/filtered-chats]
:<- [:search/filtered-browsers]
(fn [[chats browsers search-filter filtered-chats filtered-browsers]]
(if (or (nil? search-filter)
(and platform/desktop? (empty? search-filter)))
{:all-home-items
(sort-by #(-> % second :timestamp) >
(merge chats browsers))}
{:search-filter search-filter
:chats filtered-chats
:browsers filtered-browsers})))
(re-frame/reg-sub
:chain-sync-state

View File

@ -1,22 +1,23 @@
(ns status-im.ui.screens.home.views
(:require-macros [status-im.utils.views :as views])
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.i18n :as i18n]
[status-im.react-native.resources :as resources]
[status-im.ui.components.animation :as animation]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.common.common :as components.common]
[status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.ui.components.list.views :as list]
[status-im.ui.components.react :as react]
[status-im.ui.components.toolbar.view :as toolbar]
[status-im.ui.components.status-bar.view :as status-bar]
[status-im.ui.components.toolbar.actions :as toolbar.actions]
[status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.colors :as colors]
[status-im.ui.screens.home.views.inner-item :as inner-item]
[status-im.ui.components.toolbar.view :as toolbar]
[status-im.ui.screens.home.styles :as styles]
[status-im.ui.screens.home.views.inner-item :as inner-item]
[status-im.utils.platform :as platform]
[status-im.react-native.resources :as resources]
[status-im.ui.components.common.common :as components.common]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.utils.datetime :as time]
[status-im.ui.components.react :as components]
[status-im.utils.utils :as utils]
[status-im.ui.components.status-bar.view :as status-bar]))
[status-im.utils.utils :as utils])
(:require-macros [status-im.utils.views :as views]))
(defn- toolbar [show-welcome? show-sync-state sync-state latest-block-number logged-in?]
(when-not (and show-welcome?
@ -44,8 +45,8 @@
(assoc-in [:icon-opts :accessibility-label] :new-chat-button))]]
platform/ios?
[react/view {:style styles/spinner-container}
[components/activity-indicator {:color colors/blue
:animating true}]])]))
[react/activity-indicator {:color colors/blue
:animating true}]])]))
(defn- home-action-button [logged-in?]
[react/view styles/action-button-container
@ -53,8 +54,8 @@
:on-press (when logged-in? #(re-frame/dispatch [:navigate-to :new]))}
[react/view styles/action-button
(if-not logged-in?
[components/activity-indicator {:color :white
:animating true}]
[react/activity-indicator {:color :white
:animating true}]
[icons/icon :main-icons/add {:color :white}])]]])
(defn home-list-item [[home-item-id home-item]]
@ -88,15 +89,178 @@
[react/i18n-text {:style styles/welcome-text-description
:key :welcome-to-status-description}]]]))
(views/defview chats-list []
(views/letsubs [home-items [:home-items]]
(if (empty? home-items)
[react/view styles/no-chats
[react/i18n-text {:style styles/no-chats-text :key :no-recent-chats}]]
[list/flat-list {:data home-items
:key-fn first
:render-fn (fn [home-item]
[home-list-item home-item])}])))
(defn search-input
[search-filter {: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
:font-size 15}}
(i18n/label :t/cancel)]])]))))
(defonce search-input-state
(reagent/atom {:show? false
:height (animation/create-value 0)}))
(defn show-search!
[]
(swap! search-input-state assoc :show? true)
(animation/start
(animation/timing (:height @search-input-state)
{:toValue styles/search-input-height
:duration 350
:easing (.out (animation/easing)
(.-quad (animation/easing)))})))
(defn hide-search!
[]
(utils/set-timeout
#(swap! search-input-state assoc :show? false)
350)
(animation/start
(animation/timing (:height @search-input-state)
{:toValue 0
:duration 350
:easing (.in (animation/easing)
(.-quad (animation/easing)))})))
(defn animated-search-input
[search-filter]
(reagent/create-class
{:component-will-unmount
#(do (swap! search-input-state assoc :show? false)
(animation/set-value (:height @search-input-state) 0))
:reagent-render
(fn [search-filter]
(let [{:keys [show? height]} @search-input-state]
(when (or show?
search-filter)
[react/animated-view
{:style {:height height}}
[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-empty-view
[]
[react/view styles/no-chats
[react/i18n-text {:style styles/no-chats-text :key :no-recent-chats}]])
(defn home-filtered-items-list
[chats browsers]
[list/section-list
{:sections [{:title :t/chats
:data chats}
{:title :t/browsers
:data browsers}
{:title :t/messages
:data []}]
: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 (if (= title "messages")
:t/messages-search-coming-soon
:t/no-result))
:text-color colors/gray
:hide-chevron? true
:action-fn #()
:icon (case title
"messages" :main-icons/private-chat
"browsers" :main-icons/browser
"chats" :main-icons/message)
:icon-color colors/gray}]))
:render-fn (fn [home-item]
[home-list-item home-item])}])
(defn home-items-view
[search-filter chats browsers all-home-items]
(let [previous-touch (reagent/atom nil)
scrolling-from-top? (reagent/atom true)]
(fn [search-filter chats browsers all-home-items]
(if (not-empty search-filter)
[home-filtered-items-list chats browsers]
[react/view
(merge {:style {:flex 1}}
(when (and @scrolling-from-top?
(not (:show? @search-input-state)))
{:on-start-should-set-responder-capture
(fn [event]
(println :start)
(let [current-position (.-pageY (.-nativeEvent event))
current-timestamp (.-timestamp (.-nativeEvent event))]
(reset! previous-touch
[current-position current-timestamp]))
false)
:on-move-should-set-responder
(fn [event]
(let [current-position (.-pageY (.-nativeEvent event))
current-timestamp (.-timestamp (.-nativeEvent event))
[previous-position previous-timestamp] @previous-touch]
(when (and previous-position
(> 100 (- current-timestamp previous-timestamp))
(< 10 (- current-position
previous-position)))
(show-search!)))
false)}))
[list/flat-list {:data all-home-items
:key-fn first
:end-fill-color colors/white
:on-scroll-begin-drag
(fn [e]
(reset! scrolling-from-top?
;; check if scrolling up from top of list
(zero? (.-y (.-contentOffset (.-nativeEvent e))))))
:render-fn
(fn [home-item]
[home-list-item home-item])}]]))))
(views/defview home [loading?]
(views/letsubs [show-welcome? [:get-in [:accounts/create :show-welcome?]]
@ -105,7 +269,8 @@
sync-state [:chain-sync-state]
latest-block-number [:latest-block-number]
rpc-network? [:current-network-uses-rpc?]
network-initialized? [:current-network-initialized?]]
network-initialized? [:current-network-initialized?]
{:keys [search-filter chats browsers all-home-items]} [:home-items]]
{:component-did-mount
(fn [this]
(let [[_ loading?] (.. this -props -argv)]
@ -115,21 +280,29 @@
100))))}
[react/view styles/container
[status-bar/status-bar {:type :main}]
[react/view (assoc styles/container :background-color :white)
[toolbar show-welcome? (and network-initialized? (not rpc-network?)) sync-state latest-block-number (not logging-in?)]
[react/keyboard-avoiding-view {:style (assoc styles/container :background-color :white)}
[toolbar show-welcome? (and network-initialized? (not rpc-network?))
sync-state latest-block-number (not logging-in?)]
(cond show-welcome?
[welcome view-id]
loading?
[react/view {:style {:flex 1
:justify-content :center
:align-items :center}}
[connectivity/connectivity-view]
[components/activity-indicator {:flex 1
:animating true}]]
[react/activity-indicator {:flex 1
:animating true}]]
:else
[react/view {:style {:flex 1}}
[react/view {:style {:flex 1
:z-index -1}}
[connectivity/connectivity-view]
[chats-list]])
[animated-search-input search-filter]
(if (and (not search-filter)
(empty? all-home-items))
[home-empty-view]
[home-items-view search-filter chats browsers all-home-items])])
(when platform/android?
[home-action-button (not logging-in?)])]]))

View File

@ -16,14 +16,32 @@
:random-name "random-name4"
:tags #{"tag4"}}}]
(testing "no search filter"
(is (= 4 (count (search.subs/filter-chats [chats ""])))))
(is (= 0
(count (search.subs/apply-filter ""
chats
search.subs/extract-chat-attributes)))))
(testing "searching for a specific tag"
(is (= 1 (count (search.subs/filter-chats [chats "tag2"])))))
(is (= 1
(count (search.subs/apply-filter "tag2"
chats
search.subs/extract-chat-attributes)))))
(testing "searching for a partial tag"
(is (= 3 (count (search.subs/filter-chats [chats "tag"])))))
(is (= 3
(count (search.subs/apply-filter "tag"
chats
search.subs/extract-chat-attributes)))))
(testing "searching for a specific random-name"
(is (= 1 (count (search.subs/filter-chats [chats "random-name1"])))))
(is (= 1
(count (search.subs/apply-filter "random-name1"
chats
search.subs/extract-chat-attributes)))))
(testing "searching for a partial random-name"
(is (= 4 (count (search.subs/filter-chats [chats "random-name"])))))
(is (= 4
(count (search.subs/apply-filter "random-name"
chats
search.subs/extract-chat-attributes)))))
(testing "searching for a specific chat name"
(is (= 1 (count (search.subs/filter-chats [chats "name4"])))))))
(is (= 1
(count (search.subs/apply-filter "name4"
chats
search.subs/extract-chat-attributes)))))))

View File

@ -248,6 +248,7 @@
"desktop-alpha-release-warning": "Thanks for trying Status Desktop! This is an early alpha release focused on chat, and is missing several features found in the mobile client and may contain bugs and other issues. Please note that this is an alpha release and we advise you that using this app should be done for testing purposes only and you assume the full responsibility for all risks concerning your data and funds. Status makes no claims of security or integrity of funds in these builds.",
"delete-message": "Delete message",
"browser": "Browser",
"browsers": "Browsers",
"sign-message": "Sign Message",
"reorder-groups": "Reorder groups",
"currency-display-name-rub": "Russia Ruble",
@ -743,7 +744,11 @@
"your-data-belongs-to-you-description": "Status cant help you recover your account if you lose your recovery phrase. You are in charge of the security of your data, and backing up your recovery phrase is the best safeguard.",
"transactions-filter-title": "Filter history",
"view-profile": "View profile",
"search": "Search",
"message": "Message",
"messages": "Messages",
"messages-search-coming-soon": "Searching message history coming soon",
"no-result": "No results",
"here-is-your-passphrase": "Here is your passphrase, *write this down and keep this safe!* You will need it to recover your account.",
"currency-display-name-ttd": "Trinidad and Tobago Dollar",
"wallet-assets": "Assets",