[#13967] Displays filtering tabs and read/unread filter (#14105)

This commit is contained in:
Icaro Motta 2022-10-05 14:44:37 -03:00 committed by GitHub
parent 390ac858dc
commit 73983b2abd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 29 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@ -248,6 +248,8 @@
(def react-native-gradien #js {:default #js {}}) (def react-native-gradien #js {:default #js {}})
(def masked-view #js {:default #js {}})
(def react-native-permissions #js {:default #js {}}) (def react-native-permissions #js {:default #js {}})
(def push-notification-ios #js {:default #js {:abandonPermissions identity}}) (def push-notification-ios #js {:default #js {:abandonPermissions identity}})
@ -308,6 +310,7 @@
"react-native-device-info" react-native-device-info "react-native-device-info" react-native-device-info
"react-native-push-notification" react-native-push-notification "react-native-push-notification" react-native-push-notification
"react-native-linear-gradient" react-native-gradien "react-native-linear-gradient" react-native-gradien
"@react-native-community/masked-view" masked-view
"react-native-blob-util" react-native-blob-util "react-native-blob-util" react-native-blob-util
"react-native-navigation" react-native-navigation "react-native-navigation" react-native-navigation
"@react-native-community/push-notification-ios" push-notification-ios "@react-native-community/push-notification-ios" push-notification-ios

View File

@ -1,21 +1,139 @@
(ns quo2.components.tabs.tabs (ns quo2.components.tabs.tabs
(:require [reagent.core :as reagent] (:require [oops.core :refer [oget]]
[quo.react-native :as rn] [quo.react-native :as rn]
[quo2.components.tabs.tab :as tab])) [quo2.components.tabs.tab :as tab]
[reagent.core :as reagent]
[status-im.ui.components.react :as react]
[status-im.utils.number :as number-utils]))
(def default-tab-size 32)
(defn tabs [{:keys [default-active on-change]}] (defn tabs [{:keys [default-active on-change]}]
(let [active-tab-id (reagent/atom default-active)] (let [active-tab-id (reagent/atom default-active)]
(fn [{:keys [data size] :or {size 32}}] (fn [{:keys [data size] :or {size default-tab-size}}]
(let [active-id @active-tab-id] (let [active-id @active-tab-id]
[rn/view {:flex-direction :row} [rn/view {:flex-direction :row}
(for [{:keys [label id]} data] (for [{:keys [label id]} data]
^{:key id} ^{:key id}
[rn/view {:margin-right (if (= size 32) 12 8)} [rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)}}
[tab/tab [tab/tab
{:id id {:id id
:size size :size size
:active (= id active-id) :active (= id active-id)
:on-press #(do (reset! active-tab-id %) :on-press (fn [^js press-event id]
(when on-change (reset! active-tab-id id)
(on-change %)))} (when on-change
(on-change press-event id)))}
label]])])))) label]])]))))
(defn- calculate-fade-end-percentage
[{:keys [offset-x content-width layout-width max-fade-percentage]}]
(let [fade-percentage (max max-fade-percentage
(/ (+ layout-width offset-x)
content-width))]
;; Truncate to avoid unnecessary rendering.
(if (> fade-percentage 0.99)
0.99
(number-utils/naive-round fade-percentage 2))))
(defn scrollable-tabs
"Just like the component `tabs`, displays horizontally scrollable tabs with
extra options to control if/how the end of the scroll view fades.
Tabs are rendered using ReactNative's FlatList, which offers the convenient
`scrollToIndex` method. FlatList accepts VirtualizedList and ScrollView props,
and so does this component.
Usage:
[tabs/scrollable-tabs
{:scroll-on-press? true
:fade-end? true
:on-change #(...)
:default-active :tab-a
:data [{:id :tab-a :label \"Tab A\"}
{:id :tab-b :label \"Tab B\"}]}]]
Opts:
- `size` number
- `scroll-on-press?` When non-nil, clicking on a tab centers it the middle
(with animation enabled).
- `fade-end?` When non-nil, causes the end of the scrollable view to fade out.
- `fade-end-percentage` Percentage where fading starts relative to the total
layout width of the `flat-list` data."
[{:keys [default-active fade-end-percentage]
:or {fade-end-percentage 0.8}}]
(let [active-tab-id (reagent/atom default-active)
fading (reagent/atom {:fade-end-percentage fade-end-percentage})
flat-list-ref (atom nil)]
(fn [{:keys [data
fade-end-percentage
fade-end?
on-change
on-scroll
scroll-event-throttle
scroll-on-press?
size]
:or {fade-end-percentage fade-end-percentage
fade-end? false
scroll-event-throttle 64
scroll-on-press? false
size default-tab-size}
:as props}]
(let [maybe-mask-wrapper (if fade-end?
[react/masked-view
{:mask-element (reagent/as-element
[react/linear-gradient {:colors ["black" "transparent"]
:locations [(@fading :fade-end-percentage) 1]
:start {:x 0 :y 0}
:end {:x 1 :y 0}
:pointer-events :none
:style {:width "100%"
:height "100%"}}])}]
[:<>])]
(conj maybe-mask-wrapper
[rn/flat-list
(merge (dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
{:ref (partial reset! flat-list-ref)
:extra-data (str @active-tab-id)
:horizontal true
:scroll-event-throttle scroll-event-throttle
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll (fn [^js e]
(when fade-end?
(let [offset-x (oget e "nativeEvent.contentOffset.x")
content-width (oget e "nativeEvent.contentSize.width")
layout-width (oget e "nativeEvent.layoutMeasurement.width")
new-percentage (calculate-fade-end-percentage {:offset-x offset-x
:content-width content-width
:layout-width layout-width
:max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering.
(when (not= new-percentage (@fading :fade-end-percentage))
(swap! fading assoc :fade-end-percentage new-percentage))))
(when on-scroll
(on-scroll e)))
:render-fn (fn [{:keys [id label]} index]
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)
:padding-right (when (= index (dec (count data)))
(get-in props [:style :padding-left]))}}
[tab/tab {:id id
:size size
:active (= id @active-tab-id)
:on-press (fn [id]
(reset! active-tab-id id)
(when scroll-on-press?
(.scrollToIndex @flat-list-ref
#js {:animated true
:index index
:viewPosition 0.5}))
(when on-change
(on-change id)))}
label]])})])))))

View File

@ -20,13 +20,19 @@
(fn [db] (fn [db]
(get-in db [:activity-center :current-status-filter]))) (get-in db [:activity-center :current-status-filter])))
(re-frame/reg-sub
:activity-center/status-filter-unread-enabled?
:<- [:activity-center/current-status-filter]
(fn [current-status-filter]
(= :unread current-status-filter)))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/notifications-per-read-status :activity-center/notifications-per-read-status
:<- [:activity-center/notifications-read] :<- [:activity-center/notifications-read]
:<- [:activity-center/notifications-unread] :<- [:activity-center/notifications-unread]
:<- [:activity-center/current-status-filter] :<- [:activity-center/status-filter-unread-enabled?]
(fn [[notifications-read notifications-unread status-filter]] (fn [[notifications-read notifications-unread unread-filter-enabled?]]
(if (= status-filter :unread) (if unread-filter-enabled?
notifications-unread notifications-unread
notifications-read))) notifications-read)))

View File

@ -12,6 +12,7 @@
:refer (SafeAreaProvider SafeAreaInsetsContext)] :refer (SafeAreaProvider SafeAreaInsetsContext)]
["@react-native-community/clipboard" :default Clipboard] ["@react-native-community/clipboard" :default Clipboard]
["react-native-linear-gradient" :default LinearGradient] ["react-native-linear-gradient" :default LinearGradient]
["@react-native-community/masked-view" :default MaskedView]
["react-native-navigation" :refer (Navigation)] ["react-native-navigation" :refer (Navigation)]
["react-native-fast-image" :as FastImage] ["react-native-fast-image" :as FastImage]
["@react-native-community/blur" :as blur]) ["@react-native-community/blur" :as blur])
@ -40,6 +41,8 @@
(def linear-gradient (reagent/adapt-react-class LinearGradient)) (def linear-gradient (reagent/adapt-react-class LinearGradient))
(def masked-view (reagent/adapt-react-class MaskedView))
(def blur-view (reagent/adapt-react-class (.-BlurView blur))) (def blur-view (reagent/adapt-react-class (.-BlurView blur)))
(defn valid-source? [source] (defn valid-source? [source]

View File

@ -2,17 +2,21 @@
(:require [quo.components.animated.pressable :as animation] (:require [quo.components.animated.pressable :as animation]
[quo.react-native :as rn] [quo.react-native :as rn]
[quo2.components.buttons.button :as button] [quo2.components.buttons.button :as button]
[quo2.components.markdown.text :as text]
[quo2.components.notifications.activity-logs :as activity-logs] [quo2.components.notifications.activity-logs :as activity-logs]
[quo2.components.tabs.tabs :as tabs]
[quo2.components.tags.context-tags :as context-tags] [quo2.components.tags.context-tags :as context-tags]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.i18n.i18n :as i18n] [status-im.i18n.i18n :as i18n]
[status-im.multiaccounts.core :as multiaccounts] [status-im.multiaccounts.core :as multiaccounts]
[status-im.ui.components.topbar :as topbar]
[status-im.utils.datetime :as datetime] [status-im.utils.datetime :as datetime]
[status-im.utils.handlers :refer [<sub >evt]])) [status-im.utils.handlers :refer [<sub >evt]]))
(defonce selected-activity-type
(reagent/atom :activity-type/all))
(defn activity-title (defn activity-title
[{:keys [type]}] [{:keys [type]}]
(case type (case type
@ -107,24 +111,64 @@
(defn notifications-list (defn notifications-list
[] []
(let [notifications (<sub [:activity-center/notifications-per-read-status])] (let [notifications (<sub [:activity-center/notifications-per-read-status])]
[rn/flat-list {:style {:padding-horizontal 8} [rn/flat-list {:data notifications
:data notifications
:key-fn :id :key-fn :id
:on-end-reached #(>evt [:activity-center.notifications/fetch-next-page]) :on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
:render-fn render-notification}])) :render-fn render-notification}]))
(defn filter-selector-read []
(let [unread-filter-enabled? (<sub [:activity-center/status-filter-unread-enabled?])]
;; TODO: Replace the button by a Filter Selector component once available for use.
[button/button {:icon true
:type (if unread-filter-enabled? :primary :outline)
:size 32
:on-press #(if unread-filter-enabled?
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}])
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]))}
:unread]))
(defn activity-center [] (defn activity-center []
(reagent/create-class (reagent/create-class
{:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]) {:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}])
:reagent-render :reagent-render
(fn [] (fn []
[:<> (let [screen-padding 20]
[topbar/topbar {:navigation {:on-press #(>evt [:navigate-back])} [:<>
:title (i18n/label :t/activity)}] [button/button {:icon true
;; TODO(ilmotta): Temporary solution to switch between read/unread :type :grey
;; notifications while the Design team works on the mockups. :size 32
[button/button {:on-press #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}])} :style {:margin-vertical 12
"Unread"] :margin-left screen-padding}
[button/button {:on-press #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}])} :on-press #(>evt [:navigate-back])}
"Read"] :close]
[notifications-list]])})) [text/text {:size :heading-1
:weight :semi-bold
:style {:padding-horizontal screen-padding
:padding-vertical 12}}
(i18n/label :t/notifications)]
[rn/view {:flex-direction :row
:padding-vertical 12}
[rn/view {:flex 1
:align-self :stretch}
[tabs/scrollable-tabs {:size 32
:style {:padding-left screen-padding}
:fade-end-percentage 0.79
:scroll-on-press? true
:fade-end? true
:on-change (partial reset! selected-activity-type)
:default-active :activity-type/all
:data [{:id :activity-type/all :label (i18n/label :t/all)}
{:id :activity-type/admin :label (i18n/label :t/admin)}
{:id :activity-type/mention :label (i18n/label :t/mentions)}
{:id :activity-type/reply :label (i18n/label :t/replies)}
{:id :activity-type/contact-request :label (i18n/label :t/contact-requests)}
{:id :activity-type/identity-verification :label (i18n/label :t/identity-verification)}
{:id :activity-type/transaction :label (i18n/label :t/transactions)}
{:id :activity-type/membership :label (i18n/label :t/membership)}
{:id :activity-type/system :label (i18n/label :t/system)}]}]]
[rn/view {:flex-grow 0
:margin-left 16
:padding-right screen-padding}
[filter-selector-read]]]
[rn/view {:padding-horizontal screen-padding}
[notifications-list]]]))}))

View File

@ -0,0 +1,15 @@
(ns status-im.utils.number)
(defn naive-round
"Quickly and naively round number `n` up to `decimal-places`.
Example usage: use it to avoid re-renders caused by floating-point number
changes in Reagent atoms. Such numbers can be rounded up to a certain number
of `decimal-places` in order to avoid re-rendering due to tiny fractional
changes.
Don't use this function for arbitrary-precision arithmetic."
[n decimal-places]
(let [scale (Math/pow 10 decimal-places)]
(/ (Math/round (* n scale))
scale)))

View File

@ -1762,6 +1762,7 @@
"new-ui": "New UI", "new-ui": "New UI",
"send-contact-request-message": "To start a chat you need to become contacts", "send-contact-request-message": "To start a chat you need to become contacts",
"contact-request": "Contact request", "contact-request": "Contact request",
"contact-requests": "Contact Requests",
"say-hi": "Say hi", "say-hi": "Say hi",
"opened" : "Opened", "opened" : "Opened",
"accepted": "Accepted", "accepted": "Accepted",
@ -1798,5 +1799,10 @@
"edit-message": "Edit message", "edit-message": "Edit message",
"save-image-library": "Save image to library", "save-image-library": "Save image to library",
"share-image": "Share image", "share-image": "Share image",
"see-sticker-set": "See the full sticker set" "see-sticker-set": "See the full sticker set",
"mentions": "Mentions",
"admin": "Admin",
"replies": "Replies",
"identity-verification": "Identity verification",
"membership": "Membership"
} }