diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index c77112d486..17b4faf99b 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -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 {}) diff --git a/src/quo2/components/notifications/notification_dot.cljs b/src/quo2/components/notifications/notification_dot.cljs index 9436f24579..632d567d93 100644 --- a/src/quo2/components/notifications/notification_dot.cljs +++ b/src/quo2/components/notifications/notification_dot.cljs @@ -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)}]) diff --git a/src/quo2/components/tabs/segmented_tab.cljs b/src/quo2/components/tabs/segmented_tab.cljs index 8d3002f5ba..46adc2f8c2 100644 --- a/src/quo2/components/tabs/segmented_tab.cljs +++ b/src/quo2/components/tabs/segmented_tab.cljs @@ -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 diff --git a/src/quo2/components/tabs/tab.cljs b/src/quo2/components/tabs/tab.cljs deleted file mode 100644 index 3a7959f17d..0000000000 --- a/src/quo2/components/tabs/tab.cljs +++ /dev/null @@ -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)]]]))) diff --git a/src/quo2/components/tabs/tab/style.cljs b/src/quo2/components/tabs/tab/style.cljs new file mode 100644 index 0000000000..7f29da3ad6 --- /dev/null +++ b/src/quo2/components/tabs/tab/style.cljs @@ -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]))) diff --git a/src/quo2/components/tabs/tab/view.cljs b/src/quo2/components/tabs/tab/view.cljs new file mode 100644 index 0000000000..54179e9ddd --- /dev/null +++ b/src/quo2/components/tabs/tab/view.cljs @@ -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}])]])) diff --git a/src/quo2/components/tabs/tabs.cljs b/src/quo2/components/tabs/tabs.cljs index 1231bc79a6..67cf2ad193 100644 --- a/src/quo2/components/tabs/tabs.cljs +++ b/src/quo2/components/tabs/tabs.cljs @@ -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)])))) diff --git a/src/react_native/svg.cljs b/src/react_native/svg.cljs index ac5eb48cf2..e349fe8531 100644 --- a/src/react_native/svg.cljs +++ b/src/react_native/svg.cljs @@ -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)) diff --git a/src/status_im2/contexts/activity_center/events.cljs b/src/status_im2/contexts/activity_center/events.cljs index ecc8c14ab7..2631ec3abb 100644 --- a/src/status_im2/contexts/activity_center/events.cljs +++ b/src/status_im2/contexts/activity_center/events.cljs @@ -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]} diff --git a/src/status_im2/contexts/activity_center/events_test.cljs b/src/status_im2/contexts/activity_center/events_test.cljs index 7f0ad84aff..ff2581ea1d 100644 --- a/src/status_im2/contexts/activity_center/events_test.cljs +++ b/src/status_im2/contexts/activity_center/events_test.cljs @@ -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))))))))) diff --git a/src/status_im2/contexts/activity_center/view.cljs b/src/status_im2/contexts/activity_center/view.cljs index 08d966e3b5..1cb63a0208 100644 --- a/src/status_im2/contexts/activity_center/view.cljs +++ b/src/status_im2/contexts/activity_center/view.cljs @@ -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 [] diff --git a/src/status_im2/contexts/quo_preview/tabs/tabs.cljs b/src/status_im2/contexts/quo_preview/tabs/tabs.cljs index 83554962f1..82f36a546b 100644 --- a/src/status_im2/contexts/quo_preview/tabs/tabs.cljs +++ b/src/status_im2/contexts/quo_preview/tabs/tabs.cljs @@ -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}]]) diff --git a/src/status_im2/subs/activity_center.cljs b/src/status_im2/subs/activity_center.cljs index ddf9bce440..73ed041f3a 100644 --- a/src/status_im2/subs/activity_center.cljs +++ b/src/status_im2/subs/activity_center.cljs @@ -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 diff --git a/src/status_im2/subs/activity_center_test.cljs b/src/status_im2/subs/activity_center_test.cljs index 4f3598fda3..cd03600adc 100644 --- a/src/status_im2/subs/activity_center_test.cljs +++ b/src/status_im2/subs/activity_center_test.cljs @@ -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]))))