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:
parent
9ded9da7e3
commit
8626cd3e6d
|
@ -137,10 +137,13 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
|
|||
(def react-native-share #js {:default {}})
|
||||
(def react-native-svg
|
||||
#js
|
||||
{:SvgUri #js {:render identity}
|
||||
:SvgXml #js {:render identity}
|
||||
:default #js {:render identity}
|
||||
:Path #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}
|
||||
:default #js {:render identity}})
|
||||
(def react-native-webview #js {:default {}})
|
||||
(def react-native-audio-toolkit #js {:MediaStates {}})
|
||||
(def net-info #js {})
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
(:require [quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]))
|
||||
|
||||
(def ^:const size 8)
|
||||
|
||||
(defn notification-dot
|
||||
[style]
|
||||
[{:keys [style]}]
|
||||
[rn/view
|
||||
{:style (merge {:background-color (colors/theme-colors colors/primary-50 colors/primary-60)
|
||||
:width 8
|
||||
:height 8
|
||||
:border-radius 4
|
||||
:position :absolute
|
||||
:z-index 1}
|
||||
style)}])
|
||||
{:accessibility-label :notification-dot
|
||||
:style (merge
|
||||
{:background-color (colors/theme-colors colors/primary-50 colors/primary-60)
|
||||
:width size
|
||||
:height size
|
||||
:border-radius (/ size 2)
|
||||
:position :absolute
|
||||
:z-index 1}
|
||||
style)}])
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(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.theme :as theme]
|
||||
[react-native.core :as rn]
|
||||
|
@ -28,7 +28,7 @@
|
|||
[rn/view
|
||||
{:margin-left (if (= 0 indx) 0 2)
|
||||
:flex 1}
|
||||
[tab/tab
|
||||
[tab/view
|
||||
{:id id
|
||||
:segmented true
|
||||
:size size
|
||||
|
|
|
@ -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)]]])))
|
|
@ -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])))
|
|
@ -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}])]]))
|
|
@ -1,8 +1,6 @@
|
|||
(ns quo2.components.tabs.tabs
|
||||
(:require [oops.core :refer [oget]]
|
||||
[quo2.components.notifications.notification-dot :refer [notification-dot]]
|
||||
[quo2.components.tabs.tab :as tab]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.components.tabs.tab.view :as tab]
|
||||
[react-native.core :as rn]
|
||||
[react-native.linear-gradient :as linear-gradient]
|
||||
[react-native.masked-view :as masked-view]
|
||||
|
@ -11,22 +9,7 @@
|
|||
[utils.number :as utils.number]))
|
||||
|
||||
(def default-tab-size 32)
|
||||
|
||||
(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]])
|
||||
(def unread-count-offset 3)
|
||||
|
||||
(defn- calculate-fade-end-percentage
|
||||
[{:keys [offset-x content-width layout-width max-fade-percentage]}]
|
||||
|
@ -38,35 +21,100 @@
|
|||
0.99
|
||||
(utils.number/naive-round fade-percentage 2))))
|
||||
|
||||
(defn tabs
|
||||
"Usage:
|
||||
{:type :icon/:emoji/:label
|
||||
:component tag/tab
|
||||
: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."
|
||||
(defn- masked-view-wrapper
|
||||
[{:keys [fade-end-percentage fade-end?]} & children]
|
||||
(if fade-end?
|
||||
(into [masked-view/masked-view
|
||||
{:mask-element
|
||||
(reagent/as-element
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [:black :transparent]
|
||||
:locations [fade-end-percentage 1]
|
||||
:start {:x 0 :y 0}
|
||||
:end {:x 1 :y 0}
|
||||
:pointer-events :none
|
||||
:style {:width "100%"
|
||||
:height "100%"}}])}]
|
||||
children)
|
||||
(into [:<>] children)))
|
||||
|
||||
(defn- on-scroll-handler
|
||||
[{:keys [on-scroll fading fade-end-percentage fade-end?]} ^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
|
||||
(get @fading :fade-end-percentage))
|
||||
(swap! fading assoc
|
||||
:fade-end-percentage
|
||||
new-percentage))))
|
||||
(when on-scroll
|
||||
(on-scroll e)))
|
||||
|
||||
(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
|
||||
{:style {:margin-right (if (= size default-tab-size) 12 8)
|
||||
:padding-right (when (= index (dec number-of-items))
|
||||
(:padding-left style))}}
|
||||
[tab/view
|
||||
{:id id
|
||||
:notification-dot? notification-dot?
|
||||
:accessibility-label accessibility-label
|
||||
:size size
|
||||
:override-theme override-theme
|
||||
:blur? blur?
|
||||
:active (= id @active-tab-id)
|
||||
:on-press (fn [id]
|
||||
(reset! active-tab-id id)
|
||||
(when (and scroll-on-press? @flat-list-ref)
|
||||
(.scrollToIndex ^js @flat-list-ref
|
||||
#js
|
||||
{:animated true
|
||||
:index index
|
||||
:viewPosition 0.5}))
|
||||
(when on-change
|
||||
(on-change id)))}
|
||||
label]])
|
||||
|
||||
(defn tabs
|
||||
" Common options (for scrollable and non-scrollable tabs):
|
||||
|
||||
- `blur?` Boolean passed down to `quo2.components.tabs.tab/tab`.
|
||||
- `data` Vector of tab items.
|
||||
- `on-change` Callback called after a tab is selected.
|
||||
- `override-theme` Passed down to `quo2.components.tabs.tab/tab`.
|
||||
- `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)
|
||||
|
@ -78,125 +126,74 @@
|
|||
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}
|
||||
:or {fade-end-percentage fade-end-percentage
|
||||
fade-end? false
|
||||
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
|
||||
(reagent/as-element
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [:black :transparent]
|
||||
:locations [(get @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)
|
||||
(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?
|
||||
(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
|
||||
(get @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
|
||||
:override-theme override-theme
|
||||
:blur? blur?
|
||||
:active (= id @active-tab-id)
|
||||
:on-press (fn [id]
|
||||
(reset! active-tab-id id)
|
||||
(when scroll-on-press?
|
||||
(.scrollToIndex
|
||||
^js
|
||||
@flat-list-ref
|
||||
#js
|
||||
{:animated true
|
||||
:index index
|
||||
:viewPosition
|
||||
0.5}))
|
||||
(when on-change
|
||||
(on-change id)))}
|
||||
label]])})]))
|
||||
[rn/view (merge {:flex-direction :row} style)
|
||||
(doall
|
||||
(for [{:keys [label id notification-dot? accessibility-label]} data]
|
||||
^{:key id}
|
||||
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)}}
|
||||
(when notification-dot?
|
||||
[indicator])
|
||||
[tab/tab
|
||||
{:id id
|
||||
:size size
|
||||
:accessibility-label accessibility-label
|
||||
:active (= id @active-tab-id)
|
||||
:on-press (fn []
|
||||
(reset! active-tab-id id)
|
||||
(when on-change
|
||||
(on-change id)))}
|
||||
label]]))]))))
|
||||
[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
|
||||
:style style})})]]]
|
||||
[rn/view (merge style {:flex-direction :row})
|
||||
(map-indexed (fn [index item]
|
||||
^{:key (:id item)}
|
||||
[render-tab
|
||||
{:active-tab-id active-tab-id
|
||||
:blur? blur?
|
||||
:number-of-items (count data)
|
||||
:on-change on-change
|
||||
:override-theme override-theme
|
||||
:size size
|
||||
:style style}
|
||||
item
|
||||
index])
|
||||
data)]))))
|
||||
|
|
|
@ -4,3 +4,6 @@
|
|||
|
||||
(def svg (reagent/adapt-react-class Svg/default))
|
||||
(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))
|
||||
|
|
|
@ -365,16 +365,30 @@
|
|||
(rf/defn 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"
|
||||
:params [types/all-supported]
|
||||
:params [[notification-type]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success
|
||||
%])
|
||||
:on-error #()}]})
|
||||
notification-type %])
|
||||
:on-error #(rf/dispatch [:activity-center.notifications/fetch-unread-count-error
|
||||
%])}]})
|
||||
|
||||
(rf/defn notifications-fetch-unread-count-success
|
||||
{:events [:activity-center.notifications/fetch-unread-count-success]}
|
||||
[{:keys [db]} result]
|
||||
{:db (assoc-in db [:activity-center :unread-count] result)})
|
||||
[{:keys [db]} notification-type 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
|
||||
{:events [:activity-center.notifications/fetch-error]}
|
||||
|
|
|
@ -693,11 +693,26 @@
|
|||
(h/run-test-sync
|
||||
(setup)
|
||||
(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)
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/fetch-unread-count])
|
||||
|
||||
(is (= "wakuext_unreadAndAcceptedActivityCenterNotificationsCount"
|
||||
(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)))))))))
|
||||
|
|
|
@ -50,7 +50,8 @@
|
|||
|
||||
(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
|
||||
{:size 32
|
||||
:scrollable? true
|
||||
|
@ -65,22 +66,39 @@
|
|||
:default-active filter-type
|
||||
:data [{:id types/no-type
|
||||
:label (i18n/label :t/all)}
|
||||
{:id types/admin
|
||||
:label (i18n/label :t/admin)}
|
||||
{:id types/mention
|
||||
:label (i18n/label :t/mentions)}
|
||||
{:id types/reply
|
||||
:label (i18n/label :t/replies)}
|
||||
{:id types/contact-request
|
||||
:label (i18n/label :t/contact-requests)}
|
||||
{:id types/contact-verification
|
||||
:label (i18n/label :t/identity-verification)}
|
||||
{:id types/tx
|
||||
:label (i18n/label :t/transactions)}
|
||||
{:id types/membership
|
||||
:label (i18n/label :t/membership)}
|
||||
{:id types/system
|
||||
:label (i18n/label :t/system)}]}]))
|
||||
{:id types/admin
|
||||
:label (i18n/label :t/admin)
|
||||
:accessibility-label :tab-admin
|
||||
:notification-dot? (contains? types-with-unread types/admin)}
|
||||
{:id types/mention
|
||||
:label (i18n/label :t/mentions)
|
||||
:accessibility-label :tab-mention
|
||||
:notification-dot? (contains? types-with-unread types/mention)}
|
||||
{:id types/reply
|
||||
:label (i18n/label :t/replies)
|
||||
:accessibility-label :tab-reply
|
||||
:notification-dot? (contains? types-with-unread types/reply)}
|
||||
{:id types/contact-request
|
||||
:label (i18n/label :t/contact-requests)
|
||||
:accessibility-label :tab-contact-request
|
||||
:notification-dot? (contains? types-with-unread types/contact-request)}
|
||||
{:id types/contact-verification
|
||||
:label (i18n/label :t/identity-verification)
|
||||
:accessibility-label :tab-contact-verification
|
||||
:notification-dot? (contains? types-with-unread
|
||||
types/contact-verification)}
|
||||
{:id types/tx
|
||||
:label (i18n/label :t/transactions)
|
||||
:accessibility-label :tab-tx
|
||||
:notification-dot? (contains? types-with-unread types/tx)}
|
||||
{:id types/membership
|
||||
:label (i18n/label :t/membership)
|
||||
:accessibility-label :tab-membership
|
||||
:notification-dot? (contains? types-with-unread types/membership)}
|
||||
{:id types/system
|
||||
:label (i18n/label :t/system)
|
||||
:accessibility-label :tab-system
|
||||
:notification-dot? (contains? types-with-unread types/system)}]}]))
|
||||
|
||||
(defn header
|
||||
[]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(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]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
|
@ -13,10 +13,21 @@
|
|||
:value "32"}
|
||||
{:key 24
|
||||
:value "24"}]}
|
||||
{:label "Show unread indicators?"
|
||||
:key :unread-indicators?
|
||||
:type :boolean}
|
||||
{:label "Scrollable:"
|
||||
:key :scrollable?
|
||||
: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
|
||||
[]
|
||||
(let [state (reagent/atom {:size 32
|
||||
|
@ -27,16 +38,15 @@
|
|||
[rn/view {:flex 1}
|
||||
[preview/customizer state descriptor]]
|
||||
[rn/view
|
||||
{:padding-vertical 60
|
||||
:flex-direction :row
|
||||
:justify-content :center}
|
||||
[quo2/tabs
|
||||
{:padding-vertical 60
|
||||
:padding-horizontal 20
|
||||
:flex-direction :row
|
||||
:justify-content :center}
|
||||
[quo/tabs
|
||||
(merge @state
|
||||
{:default-active 1
|
||||
:data [{:id 1 :label "Tab 1"}
|
||||
{:id 2 :label "Tab 2"}
|
||||
{:id 3 :label "Tab 3"}
|
||||
{:id 4 :label "Tab 4"}]
|
||||
:data (generate-tab-items (if (:scrollable? @state) 15 4)
|
||||
(:unread-indicators? @state))
|
||||
:on-change #(println "Active tab" %)}
|
||||
(when (:scrollable? @state)
|
||||
{:scroll-on-press? true
|
||||
|
@ -49,7 +59,7 @@
|
|||
{:background-color (colors/theme-colors colors/white colors/neutral-90)
|
||||
:flex 1}
|
||||
[rn/flat-list
|
||||
{:flex 1
|
||||
:keyboardShouldPersistTaps :always
|
||||
:header [cool-preview]
|
||||
:key-fn str}]])
|
||||
{:flex 1
|
||||
:keyboard-should-persist-taps :always
|
||||
:header [cool-preview]
|
||||
:key-fn str}]])
|
||||
|
|
|
@ -9,10 +9,30 @@
|
|||
(:notifications activity-center)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:activity-center/unread-count
|
||||
:activity-center/unread-counts-by-type
|
||||
:<- [: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
|
||||
:activity-center/filter-status
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
(ns status-im2.subs.activity-center-test
|
||||
(:require [cljs.test :refer [is testing]]
|
||||
[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
|
||||
[test-helpers.unit :as h]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(h/deftest-sub :activity-center/filter-status-unread-enabled?
|
||||
|
@ -14,3 +15,51 @@
|
|||
(testing "returns false when filter status is not unread"
|
||||
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all)
|
||||
(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]))))
|
||||
|
|
Loading…
Reference in New Issue