Show unread indicator on Activity Center tabs (#14883)

Fixes https://github.com/status-im/status-mobile/issues/14852
Fixes https://github.com/status-im/status-mobile/issues/14882

Summary
=======

- [x] Fixes https://github.com/status-im/status-mobile/issues/14852
- [x] Fixes https://github.com/status-im/status-mobile/issues/14882
- [x] Refactors `tab` component to follow new guidelines.
- [x] Improves preview area for tabs to optionally show notification dots.
- [x] Refactors `tabs` component: break the component into smaller chunks;
remove duplication between scrollable & non-scrollable tabs; update outdated
docstring;
- [x] Adds accessibility labels to tabs.

Cutout with light theme on the background:
https://user-images.githubusercontent.com/46027/214553974-2b2e8c6e-71b8-46f3-ba75-a66cb4755490.png

Cutout with dark theme on the background:
https://user-images.githubusercontent.com/46027/214554602-d637d4d6-3b20-4aa8-a2ef-64836f4454bb.png

Cutout in non-scrollable tabs:
https://user-images.githubusercontent.com/46027/214555646-bbb85546-20d9-48bb-8273-716564fb5e0f.png

Notification dots in preview area:
https://user-images.githubusercontent.com/46027/214556532-10a044ae-9df5-4c86-be5f-447955276bfd.png
This commit is contained in:
Icaro Motta 2023-02-01 13:10:57 -03:00 committed by GitHub
parent 9ded9da7e3
commit 8626cd3e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 543 additions and 324 deletions

View File

@ -137,10 +137,13 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
(def react-native-share #js {:default {}}) (def react-native-share #js {:default {}})
(def react-native-svg (def react-native-svg
#js #js
{:SvgUri #js {:render identity} {:ClipPath #js {:render identity}
:Defs #js {:render identity}
:Path #js {:render identity}
:Rect #js {:render identity}
:SvgUri #js {:render identity}
:SvgXml #js {:render identity} :SvgXml #js {:render identity}
:default #js {:render identity} :default #js {:render identity}})
:Path #js {:render identity}})
(def react-native-webview #js {:default {}}) (def react-native-webview #js {:default {}})
(def react-native-audio-toolkit #js {:MediaStates {}}) (def react-native-audio-toolkit #js {:MediaStates {}})
(def net-info #js {}) (def net-info #js {})

View File

@ -2,13 +2,17 @@
(:require [quo2.foundations.colors :as colors] (:require [quo2.foundations.colors :as colors]
[react-native.core :as rn])) [react-native.core :as rn]))
(def ^:const size 8)
(defn notification-dot (defn notification-dot
[style] [{:keys [style]}]
[rn/view [rn/view
{:style (merge {:background-color (colors/theme-colors colors/primary-50 colors/primary-60) {:accessibility-label :notification-dot
:width 8 :style (merge
:height 8 {:background-color (colors/theme-colors colors/primary-50 colors/primary-60)
:border-radius 4 :width size
:height size
:border-radius (/ size 2)
:position :absolute :position :absolute
:z-index 1} :z-index 1}
style)}]) style)}])

View File

@ -1,5 +1,5 @@
(ns quo2.components.tabs.segmented-tab (ns quo2.components.tabs.segmented-tab
(:require [quo2.components.tabs.tab :as tab] (:require [quo2.components.tabs.tab.view :as tab]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[quo2.theme :as theme] [quo2.theme :as theme]
[react-native.core :as rn] [react-native.core :as rn]
@ -28,7 +28,7 @@
[rn/view [rn/view
{:margin-left (if (= 0 indx) 0 2) {:margin-left (if (= 0 indx) 0 2)
:flex 1} :flex 1}
[tab/tab [tab/view
{:id id {:id id
:segmented true :segmented true
:size size :size size

View File

@ -1,109 +0,0 @@
(ns quo2.components.tabs.tab
(:require [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[quo2.theme :as theme]
[react-native.core :as rn]))
(def themes
{:light {:default {:background-color colors/neutral-20
:icon-color colors/neutral-50
:label {:style {:color colors/neutral-100}}}
:active {:background-color colors/neutral-50
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-20
:icon-color colors/neutral-50
:label {:style {:color colors/neutral-100}}}}
:dark {:default {:background-color colors/neutral-80
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}
:active {:background-color colors/neutral-60
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-80
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}}})
(def themes-for-blur-background
{:light {:default {:background-color colors/neutral-80-opa-5
:icon-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}}
:active {:background-color colors/neutral-80-opa-60
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-80-opa-5
:icon-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}}}
:dark {:default {:background-color colors/white-opa-5
:icon-color colors/white
:label {:style {:color colors/white}}}
:active {:background-color colors/white-opa-20
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/white-opa-5
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}}})
(defn style-container
[size disabled background-color]
(merge {:height size
:align-items :center
:justify-content :center
:flex-direction :row
:border-radius (case size
32 10
28 8
24 8
20 6)
:background-color background-color
:padding-horizontal (case size
32 12
28 12
24 8
20 8)}
(when disabled
{:opacity 0.3})))
(defn tab
"[tab opts \"label\"]
opts
{:type :primary/:secondary/:grey/:outline/:ghost/:danger
:size 40/32/24
:icon true/false
:before :icon-keyword
:after :icon-keyword}"
[_ _]
(fn [{:keys [id on-press disabled size before active accessibility-label blur? override-theme]
:or {size 32}}
children]
(let [state (cond disabled :disabled
active :active
:else :default)
{:keys [icon-color background-color label]}
(get-in (if blur? themes-for-blur-background themes)
[(or override-theme (theme/get-theme)) state])]
[rn/touchable-without-feedback
(merge {:disabled disabled
:accessibility-label accessibility-label}
(when on-press
{:on-press (fn []
(on-press id))}))
[rn/view {:style (style-container size disabled background-color)}
(when before
[rn/view
[icons/icon before {:color icon-color}]])
[rn/view
(cond
(string? children)
[text/text
(merge {:size (case size
24 :paragraph-2
20 :label
nil)
:weight :medium
:number-of-lines 1}
label)
children]
(vector? children)
children)]]])))

View File

@ -0,0 +1,101 @@
(ns quo2.components.tabs.tab.style
(:require [quo2.foundations.colors :as colors]
[quo2.theme :as theme]))
(def tab-background-opacity 0.3)
(defn size->padding-left
[size]
(case size
32 12
28 12
24 8
20 8
nil))
(defn size->border-radius
[size]
(case size
32 10
28 8
24 8
20 6
nil))
(def notification-dot
{:position :absolute
:top -2
:right -2})
(def container
{:flex-direction :row})
(defn tab
[{:keys [size disabled background-color show-notification-dot?]}]
(let [border-radius (size->border-radius size)
padding (size->padding-left size)]
(merge {:height size
:align-items :center
:flex-direction :row
:border-top-left-radius border-radius
:border-bottom-left-radius border-radius
:background-color background-color
:padding-left padding}
;; The minimum padding right of 1 is a mandatory workaround. Without
;; it, the SVG rendered besides the tab will have a 1px margin. This
;; issue still exists in the latest react-native-svg versions.
(if show-notification-dot?
{:padding-right 1}
{:border-radius border-radius
:padding-right padding})
(when disabled
{:opacity tab-background-opacity}))))
(def themes
{:light {:default {:background-color colors/neutral-20
:icon-color colors/neutral-50
:label {:style {:color colors/neutral-100}}}
:active {:background-color colors/neutral-50
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-20
:icon-color colors/neutral-50
:label {:style {:color colors/neutral-100}}}}
:dark {:default {:background-color colors/neutral-80
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}
:active {:background-color colors/neutral-60
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-80
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}}})
(def themes-for-blur-background
{:light {:default {:background-color colors/neutral-80-opa-5
:icon-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}}
:active {:background-color colors/neutral-80-opa-60
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/neutral-80-opa-5
:icon-color colors/neutral-80-opa-40
:label {:style {:color colors/neutral-100}}}}
:dark {:default {:background-color colors/white-opa-5
:icon-color colors/white
:label {:style {:color colors/white}}}
:active {:background-color colors/white-opa-20
:icon-color colors/white
:label {:style {:color colors/white}}}
:disabled {:background-color colors/white-opa-5
:icon-color colors/neutral-40
:label {:style {:color colors/white}}}}})
(defn by-theme
[{:keys [override-theme disabled active blur?]}]
(let [state (cond disabled :disabled
active :active
:else :default)
theme (or override-theme (theme/get-theme))]
(get-in (if blur? themes-for-blur-background themes)
[theme state])))

View File

@ -0,0 +1,94 @@
(ns quo2.components.tabs.tab.view
(:require [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.components.tabs.tab.style :as style]
[quo2.components.notifications.notification-dot :as notification-dot]
[react-native.core :as rn]
[react-native.svg :as svg]))
(defn- right-side-with-cutout
"SVG exported from Figma."
[{:keys [height width background-color disabled]}]
;; Do not add a view-box property, it'll cause an artifact where the SVG is
;; rendered slightly smaller than the proper width and height.
[svg/svg
{:width width
:height height
:fill background-color
:fill-opacity (when disabled style/tab-background-opacity)}
[svg/path
{:d
"M 11.468 6.781 C 11.004 6.923 10.511 7 10 7 C 7.239 7 5 4.761 5 2 C 5
1.489 5.077 0.996 5.219 0.532 C 4.687 0.351 4.134 0.213 3.564 0.123 C 2.787
0 1.858 0 0 0 L 0 32 C 1.858 32 2.787 32 3.564 31.877 C 7.843 31.199 11.199
27.843 11.877 23.564 C 12 22.787 12 21.858 12 20 L 12 12 C 12 10.142 12
9.213 11.877 8.436 C 11.787 7.866 11.649 7.313 11.468 6.781 Z"
:clip-path "url(#clip0_5514_84289)"}]
[svg/defs
[svg/clippath {:id "clip0_5514_84289"}
[svg/rect {:width width :height height :fill :none}]]]])
(defn- content
[{:keys [size label]} children]
[rn/view
(cond
(string? children)
[text/text
(merge {:size (case size
24 :paragraph-2
20 :label
nil)
:weight :medium
:number-of-lines 1}
label)
children]
(vector? children)
children)])
(defn view
[{:keys [accessibility-label
active
before
blur?
disabled
id
on-press
override-theme
size
notification-dot?]
:or {size 32}}
children]
(let [show-notification-dot? (and notification-dot? (= size 32))
{:keys [icon-color
background-color
label]}
(style/by-theme {:override-theme override-theme
:blur? blur?
:disabled disabled
:active active})]
[rn/touchable-without-feedback
(merge {:disabled disabled
:accessibility-label accessibility-label}
(when on-press
{:on-press (fn []
(on-press id))}))
[rn/view {:style style/container}
(when show-notification-dot?
[notification-dot/notification-dot
{:style style/notification-dot}])
[rn/view
{:style (style/tab {:size size
:disabled disabled
:background-color background-color
:show-notification-dot? show-notification-dot?})}
(when before
[rn/view
[icons/icon before {:color icon-color}]])
[content {:size size :label label} children]]
(when show-notification-dot?
[right-side-with-cutout
{:width (style/size->padding-left size)
:height size
:disabled disabled
:background-color background-color}])]]))

View File

@ -1,8 +1,6 @@
(ns quo2.components.tabs.tabs (ns quo2.components.tabs.tabs
(:require [oops.core :refer [oget]] (:require [oops.core :refer [oget]]
[quo2.components.notifications.notification-dot :refer [notification-dot]] [quo2.components.tabs.tab.view :as tab]
[quo2.components.tabs.tab :as tab]
[quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient] [react-native.linear-gradient :as linear-gradient]
[react-native.masked-view :as masked-view] [react-native.masked-view :as masked-view]
@ -11,22 +9,7 @@
[utils.number :as utils.number])) [utils.number :as utils.number]))
(def default-tab-size 32) (def default-tab-size 32)
(def unread-count-offset 3)
(defn indicator
[]
[rn/view
{:accessibility-label :notification-dot
:style {:position :absolute
:z-index 1
:right -2
:top -2
:width 10
:height 10
:border-radius 5
:justify-content :center
:align-items :center
:background-color (colors/theme-colors colors/neutral-5 colors/neutral-95)}}
[notification-dot]])
(defn- calculate-fade-end-percentage (defn- calculate-fade-end-percentage
[{:keys [offset-x content-width layout-width max-fade-percentage]}] [{:keys [offset-x content-width layout-width max-fade-percentage]}]
@ -38,109 +21,30 @@
0.99 0.99
(utils.number/naive-round fade-percentage 2)))) (utils.number/naive-round fade-percentage 2))))
(defn tabs (defn- masked-view-wrapper
"Usage: [{:keys [fade-end-percentage fade-end?]} & children]
{:type :icon/:emoji/:label (if fade-end?
:component tag/tab (into [masked-view/masked-view
:size 32/24
:on-press fn
:blurred? true/false
:labelled? true/false
:disabled? true/false
:scrollable? false
:scroll-on-press? true
:fade-end? true
:on-change fn
:default-active tag-id
:data [{:id :label \"\" :resource \"url\"}
{:id :label \"\" :resource \"url\"}]}
Opts:
- `component` this is to determine which component is to be rendered since the
logic in this view is shared between tab and tag component
- `blurred` boolean: use to determine border color if the background is blurred
- `type` can be icon or emoji with or without a tag label
- `labelled` boolean: is true if tag has label else false
- `size` number
- `scroll-on-press?` When non-nil, clicking on a tag 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?
scrollable?
style
size
blur?
override-theme]
:or {fade-end-percentage fade-end-percentage
fade-end? false
scroll-event-throttle 64
scrollable? false
scroll-on-press? false
size default-tab-size}
:as props}]
(if scrollable?
(let [maybe-mask-wrapper (if fade-end?
[masked-view/masked-view
{:mask-element {:mask-element
(reagent/as-element (reagent/as-element
[linear-gradient/linear-gradient [linear-gradient/linear-gradient
{:colors [:black :transparent] {:colors [:black :transparent]
:locations [(get @fading :fade-end-percentage) 1] :locations [fade-end-percentage 1]
:start {:x 0 :y 0} :start {:x 0 :y 0}
:end {:x 1 :y 0} :end {:x 1 :y 0}
:pointer-events :none :pointer-events :none
:style {:width "100%" :style {:width "100%"
:height "100%"}}])}] :height "100%"}}])}]
[:<>])] children)
(conj (into [:<>] children)))
maybe-mask-wrapper
[rn/flat-list (defn- on-scroll-handler
(merge [{:keys [on-scroll fading fade-end-percentage fade-end?]} ^js e]
(dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils.collection/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(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-to-index-failed identity
:on-scroll (fn [^js e]
(when fade-end? (when fade-end?
(let [offset-x (oget (let [offset-x (oget e "nativeEvent.contentOffset.x")
e content-width (oget e "nativeEvent.contentSize.width")
"nativeEvent.contentOffset.x") layout-width (oget e "nativeEvent.layoutMeasurement.width")
content-width new-percentage (calculate-fade-end-percentage
(oget
e
"nativeEvent.contentSize.width")
layout-width
(oget e
"nativeEvent.layoutMeasurement.width")
new-percentage
(calculate-fade-end-percentage
{:offset-x offset-x {:offset-x offset-x
:content-width content-width :content-width content-width
:layout-width layout-width :layout-width layout-width
@ -153,50 +57,143 @@
new-percentage)))) new-percentage))))
(when on-scroll (when on-scroll
(on-scroll e))) (on-scroll e)))
:render-fn (fn [{:keys [id label]} index]
(defn- render-tab
[{:keys [active-tab-id
blur?
flat-list-ref
number-of-items
on-change
override-theme
scroll-on-press?
size
style]}
{:keys [id label notification-dot? accessibility-label]}
index]
[rn/view [rn/view
{:style {:margin-right (if (= size default-tab-size) {:style {:margin-right (if (= size default-tab-size) 12 8)
12 :padding-right (when (= index (dec number-of-items))
8) (:padding-left style))}}
:padding-right (when (= index [tab/view
(dec (count data)))
(get-in props
[:style
:padding-left]))}}
[tab/tab
{:id id {:id id
:notification-dot? notification-dot?
:accessibility-label accessibility-label
:size size :size size
:override-theme override-theme :override-theme override-theme
:blur? blur? :blur? blur?
:active (= id @active-tab-id) :active (= id @active-tab-id)
:on-press (fn [id] :on-press (fn [id]
(reset! active-tab-id id) (reset! active-tab-id id)
(when scroll-on-press? (when (and scroll-on-press? @flat-list-ref)
(.scrollToIndex (.scrollToIndex ^js @flat-list-ref
^js
@flat-list-ref
#js #js
{:animated true {:animated true
:index index :index index
:viewPosition :viewPosition 0.5}))
0.5}))
(when on-change (when on-change
(on-change id)))} (on-change id)))}
label]])})])) label]])
[rn/view (merge {:flex-direction :row} style)
(doall (defn tabs
(for [{:keys [label id notification-dot? accessibility-label]} data] " Common options (for scrollable and non-scrollable tabs):
^{:key id}
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)}} - `blur?` Boolean passed down to `quo2.components.tabs.tab/tab`.
(when notification-dot? - `data` Vector of tab items.
[indicator]) - `on-change` Callback called after a tab is selected.
[tab/tab - `override-theme` Passed down to `quo2.components.tabs.tab/tab`.
{:id id - `size` 32/24
- `style` Style map passed to View wrapping tabs or to the FlatList when tabs
are scrollable.
Options for scrollable tabs:
- `fade-end-percentage` Percentage where fading starts relative to the total
layout width of the `flat-list` data.
- `fade-end?` When non-nil, causes the end of the scrollable view to fade out.
- `on-scroll` Callback called on the on-scroll event of the FlatList. Only
used when `scrollable?` is non-nil.
- `scrollable?` When non-nil, use a scrollable flat-list to render tabs.
- `scroll-on-press?` When non-nil, clicking on a tag centers it the middle
(with animation enabled).
"
[{: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-on-press?
scrollable?
style
size
blur?
override-theme]
:or {fade-end-percentage fade-end-percentage
fade-end? false
scrollable? false
scroll-on-press? false
size default-tab-size}
:as props}]
(if scrollable?
[rn/view {:style {:margin-top (- (dec unread-count-offset))}}
[masked-view-wrapper
{:fade-end-percentage (get @fading :fade-end-percentage) :fade-end? fade-end?}
[rn/flat-list
(merge
(dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils.collection/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:style style
;; The padding-top workaround is needed because on Android
;; {:overflow :visible} doesn't work on components inheriting
;; from ScrollView (e.g. FlatList). There are open issues, here's
;; just one about this topic:
;; https://github.com/facebook/react-native/issues/3121
:content-container-style {:padding-top (dec unread-count-offset)}
:extra-data (str @active-tab-id)
:horizontal true
:scroll-event-throttle 64
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll-to-index-failed identity
:on-scroll (partial on-scroll-handler
{:fade-end-percentage fade-end-percentage
:fade-end? fade-end?
:fading fading
:on-scroll on-scroll})
:render-fn (partial render-tab
{:active-tab-id active-tab-id
:blur? blur?
:flat-list-ref flat-list-ref
:number-of-items (count data)
:on-change on-change
:override-theme override-theme
:scroll-on-press? scroll-on-press?
:size size :size size
:accessibility-label accessibility-label :style style})})]]]
:active (= id @active-tab-id) [rn/view (merge style {:flex-direction :row})
:on-press (fn [] (map-indexed (fn [index item]
(reset! active-tab-id id) ^{:key (:id item)}
(when on-change [render-tab
(on-change id)))} {:active-tab-id active-tab-id
label]]))])))) :blur? blur?
:number-of-items (count data)
:on-change on-change
:override-theme override-theme
:size size
:style style}
item
index])
data)]))))

View File

@ -4,3 +4,6 @@
(def svg (reagent/adapt-react-class Svg/default)) (def svg (reagent/adapt-react-class Svg/default))
(def path (reagent/adapt-react-class Svg/Path)) (def path (reagent/adapt-react-class Svg/Path))
(def rect (reagent/adapt-react-class Svg/Rect))
(def clippath (reagent/adapt-react-class Svg/ClipPath))
(def defs (reagent/adapt-react-class Svg/Defs))

View File

@ -365,16 +365,30 @@
(rf/defn notifications-fetch-unread-count (rf/defn notifications-fetch-unread-count
{:events [:activity-center.notifications/fetch-unread-count]} {:events [:activity-center.notifications/fetch-unread-count]}
[_] [_]
{:dispatch-n (mapv (fn [notification-type]
[:activity-center.notifications/fetch-unread-count-for-type notification-type])
types/all-supported)})
(rf/defn notifications-fetch-unread-count-for-type
{:events [:activity-center.notifications/fetch-unread-count-for-type]}
[_ notification-type]
{:json-rpc/call [{:method "wakuext_unreadAndAcceptedActivityCenterNotificationsCount" {:json-rpc/call [{:method "wakuext_unreadAndAcceptedActivityCenterNotificationsCount"
:params [types/all-supported] :params [[notification-type]]
:on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success :on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success
%]) notification-type %])
:on-error #()}]}) :on-error #(rf/dispatch [:activity-center.notifications/fetch-unread-count-error
%])}]})
(rf/defn notifications-fetch-unread-count-success (rf/defn notifications-fetch-unread-count-success
{:events [:activity-center.notifications/fetch-unread-count-success]} {:events [:activity-center.notifications/fetch-unread-count-success]}
[{:keys [db]} result] [{:keys [db]} notification-type result]
{:db (assoc-in db [:activity-center :unread-count] result)}) {:db (assoc-in db [:activity-center :unread-counts-by-type notification-type] result)})
(rf/defn notifications-fetch-unread-count-error
{:events [:activity-center.notifications/fetch-unread-count-error]}
[_ error]
(log/error "Failed to fetch count of notifications" {:error error})
nil)
(rf/defn notifications-fetch-error (rf/defn notifications-fetch-error
{:events [:activity-center.notifications/fetch-error]} {:events [:activity-center.notifications/fetch-error]}

View File

@ -693,11 +693,26 @@
(h/run-test-sync (h/run-test-sync
(setup) (setup)
(let [spy-queue (atom [])] (let [spy-queue (atom [])]
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly 9)) (h/stub-fx-with-callbacks :json-rpc/call
:on-success
(fn [{:keys [params]}]
(if (= types/mention (ffirst params))
9
0)))
(h/spy-fx spy-queue :json-rpc/call) (h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:activity-center.notifications/fetch-unread-count]) (rf/dispatch [:activity-center.notifications/fetch-unread-count])
(is (= "wakuext_unreadAndAcceptedActivityCenterNotificationsCount" (is (= "wakuext_unreadAndAcceptedActivityCenterNotificationsCount"
(get-in @spy-queue [0 :args 0 :method]))) (get-in @spy-queue [0 :args 0 :method])))
(is (= 9 (get-in (h/db) [:activity-center :unread-count])))))))
(let [actual (get-in (h/db) [:activity-center :unread-counts-by-type])]
(is (= {types/one-to-one-chat 0
types/private-group-chat 0
types/contact-verification 0
types/contact-request 0
types/mention 9
types/reply 0
types/admin 0}
actual))
(is (= types/all-supported (set (keys actual)))))))))

View File

@ -50,7 +50,8 @@
(defn tabs (defn tabs
[] []
(let [filter-type (rf/sub [:activity-center/filter-type])] (let [filter-type (rf/sub [:activity-center/filter-type])
types-with-unread (rf/sub [:activity-center/notification-types-with-unread])]
[quo/tabs [quo/tabs
{:size 32 {:size 32
:scrollable? true :scrollable? true
@ -66,21 +67,38 @@
:data [{:id types/no-type :data [{:id types/no-type
:label (i18n/label :t/all)} :label (i18n/label :t/all)}
{:id types/admin {:id types/admin
:label (i18n/label :t/admin)} :label (i18n/label :t/admin)
:accessibility-label :tab-admin
:notification-dot? (contains? types-with-unread types/admin)}
{:id types/mention {:id types/mention
:label (i18n/label :t/mentions)} :label (i18n/label :t/mentions)
:accessibility-label :tab-mention
:notification-dot? (contains? types-with-unread types/mention)}
{:id types/reply {:id types/reply
:label (i18n/label :t/replies)} :label (i18n/label :t/replies)
:accessibility-label :tab-reply
:notification-dot? (contains? types-with-unread types/reply)}
{:id types/contact-request {:id types/contact-request
:label (i18n/label :t/contact-requests)} :label (i18n/label :t/contact-requests)
:accessibility-label :tab-contact-request
:notification-dot? (contains? types-with-unread types/contact-request)}
{:id types/contact-verification {:id types/contact-verification
:label (i18n/label :t/identity-verification)} :label (i18n/label :t/identity-verification)
:accessibility-label :tab-contact-verification
:notification-dot? (contains? types-with-unread
types/contact-verification)}
{:id types/tx {:id types/tx
:label (i18n/label :t/transactions)} :label (i18n/label :t/transactions)
:accessibility-label :tab-tx
:notification-dot? (contains? types-with-unread types/tx)}
{:id types/membership {:id types/membership
:label (i18n/label :t/membership)} :label (i18n/label :t/membership)
:accessibility-label :tab-membership
:notification-dot? (contains? types-with-unread types/membership)}
{:id types/system {:id types/system
:label (i18n/label :t/system)}]}])) :label (i18n/label :t/system)
:accessibility-label :tab-system
:notification-dot? (contains? types-with-unread types/system)}]}]))
(defn header (defn header
[] []

View File

@ -1,5 +1,5 @@
(ns status-im2.contexts.quo-preview.tabs.tabs (ns status-im2.contexts.quo-preview.tabs.tabs
(:require [quo2.components.tabs.tabs :as quo2] (:require [quo2.core :as quo]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[reagent.core :as reagent] [reagent.core :as reagent]
@ -13,10 +13,21 @@
:value "32"} :value "32"}
{:key 24 {:key 24
:value "24"}]} :value "24"}]}
{:label "Show unread indicators?"
:key :unread-indicators?
:type :boolean}
{:label "Scrollable:" {:label "Scrollable:"
:key :scrollable? :key :scrollable?
:type :boolean}]) :type :boolean}])
(defn generate-tab-items
[length unread-indicators?]
(for [index (range length)]
^{:key index}
{:id index
:label (str "Tab " (inc index))
:notification-dot? (and unread-indicators? (zero? (rem index 2)))}))
(defn cool-preview (defn cool-preview
[] []
(let [state (reagent/atom {:size 32 (let [state (reagent/atom {:size 32
@ -28,15 +39,14 @@
[preview/customizer state descriptor]] [preview/customizer state descriptor]]
[rn/view [rn/view
{:padding-vertical 60 {:padding-vertical 60
:padding-horizontal 20
:flex-direction :row :flex-direction :row
:justify-content :center} :justify-content :center}
[quo2/tabs [quo/tabs
(merge @state (merge @state
{:default-active 1 {:default-active 1
:data [{:id 1 :label "Tab 1"} :data (generate-tab-items (if (:scrollable? @state) 15 4)
{:id 2 :label "Tab 2"} (:unread-indicators? @state))
{:id 3 :label "Tab 3"}
{:id 4 :label "Tab 4"}]
:on-change #(println "Active tab" %)} :on-change #(println "Active tab" %)}
(when (:scrollable? @state) (when (:scrollable? @state)
{:scroll-on-press? true {:scroll-on-press? true
@ -50,6 +60,6 @@
:flex 1} :flex 1}
[rn/flat-list [rn/flat-list
{:flex 1 {:flex 1
:keyboardShouldPersistTaps :always :keyboard-should-persist-taps :always
:header [cool-preview] :header [cool-preview]
:key-fn str}]]) :key-fn str}]])

View File

@ -9,10 +9,30 @@
(:notifications activity-center))) (:notifications activity-center)))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/unread-count :activity-center/unread-counts-by-type
:<- [:activity-center] :<- [:activity-center]
(fn [activity-center] (fn [activity-center]
(:unread-count activity-center))) (:unread-counts-by-type activity-center)))
(re-frame/reg-sub
:activity-center/notification-types-with-unread
:<- [:activity-center/unread-counts-by-type]
(fn [unread-counts]
(reduce-kv
(fn [acc notification-type unread-count]
(if (pos? unread-count)
(conj acc notification-type)
acc))
#{}
unread-counts)))
(re-frame/reg-sub
:activity-center/unread-count
:<- [:activity-center/unread-counts-by-type]
(fn [unread-counts]
(->> unread-counts
vals
(reduce + 0))))
(re-frame/reg-sub (re-frame/reg-sub
:activity-center/filter-status :activity-center/filter-status

View File

@ -1,8 +1,9 @@
(ns status-im2.subs.activity-center-test (ns status-im2.subs.activity-center-test
(:require [cljs.test :refer [is testing]] (:require [cljs.test :refer [is testing]]
[re-frame.db :as rf-db] [re-frame.db :as rf-db]
[test-helpers.unit :as h] [status-im2.contexts.activity-center.notification-types :as types]
status-im2.subs.activity-center status-im2.subs.activity-center
[test-helpers.unit :as h]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(h/deftest-sub :activity-center/filter-status-unread-enabled? (h/deftest-sub :activity-center/filter-status-unread-enabled?
@ -14,3 +15,51 @@
(testing "returns false when filter status is not unread" (testing "returns false when filter status is not unread"
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all) (swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all)
(is (false? (rf/sub [sub-name]))))) (is (false? (rf/sub [sub-name])))))
(h/deftest-sub :activity-center/notification-types-with-unread
[sub-name]
(testing "returns an empty set when no types have unread notifications"
(swap! rf-db/app-db assoc-in
[:activity-center :unread-counts-by-type]
{types/one-to-one-chat 0
types/private-group-chat 0
types/contact-verification 0
types/contact-request 0
types/mention 0
types/reply 0
types/admin 0})
(is (= #{} (rf/sub [sub-name]))))
(testing "returns a set with all types containing positive unread counts"
(swap! rf-db/app-db assoc-in
[:activity-center :unread-counts-by-type]
{types/one-to-one-chat 1
types/private-group-chat 0
types/contact-verification 1
types/contact-request 0
types/mention 3
types/reply 0
types/admin 2})
(let [actual (rf/sub [sub-name])]
(is (= #{types/one-to-one-chat
types/contact-verification
types/mention
types/admin}
actual))
(is (set? actual)))))
(h/deftest-sub :activity-center/unread-count
[sub-name]
(swap! rf-db/app-db assoc-in
[:activity-center :unread-counts-by-type]
{types/one-to-one-chat 1
types/private-group-chat 2
types/contact-verification 3
types/contact-request 4
types/mention 5
types/reply 6
types/admin 7})
(is (= 28 (rf/sub [sub-name]))))