mirror of
https://github.com/status-im/status-react.git
synced 2025-02-26 17:41:03 +00:00
Show AC unread indicator with counter and seen state color (#15304)
- Display Activity Center unread badge with the unread counter. - Use the new seen state stored in `status-go` to change the color of the notification. - Performance: split the `top-nav` component into left and right section components and render the unread indicator in a separate component to not trigger the re-render of the entire `top-nav` (as was before). Fixes https://github.com/status-im/status-mobile/issues/14851 Demo: https://user-images.githubusercontent.com/46027/224299978-770dd5f1-302b-4375-af2b-3cd181ffdc9d.webm Notes ===== - Fix/improve: `quo/counter` displayed `NaN` to the user if the input value was an empty string. - In Figma, there's a border around the unread indicator. I didn't implement this because the ideal solution IMO involves changing the `quo/counter` component a little bit because the width of the component varies according to the content displayed (1, 9, 99, 100, etc) and I wanted to the right thing in a separate PR. Design notes ============ There's an ongoing conversation with the Design team to decide what to do with the gray indicator on top of the bell icon, since there's little contrast when it's is in the `seen` state. Platforms ========= - Android - iOS Steps to test ============= - Open Status - Receive one or more notifications in the Home screen and check the unread indicator is blue and has a counter. - Open the AC and close it, notice the unread indicator is now in the `seen` state. You can close the app and re-open and the state is persisted. - Mark notifications as read/unread at will, check the unread counter is correct.
This commit is contained in:
parent
f640eb8c8f
commit
e8556a9abf
@ -1,34 +1,29 @@
|
|||||||
(ns quo2.components.counter.--tests--.counter-component-spec
|
(ns quo2.components.counter.--tests--.counter-component-spec
|
||||||
(:require ["@testing-library/react-native" :as rtl]
|
(:require [quo2.components.counter.counter :as counter]
|
||||||
[quo2.components.counter.counter :as counter]
|
[test-helpers.component :as h]))
|
||||||
[reagent.core :as reagent]))
|
|
||||||
|
|
||||||
(defn render-counter
|
(h/describe "counter component"
|
||||||
([]
|
(h/test "default render of counter component"
|
||||||
(render-counter {} nil))
|
(h/render [counter/counter {} nil])
|
||||||
([opts value]
|
(-> (h/expect (h/get-by-test-id :counter-component))
|
||||||
(rtl/render (reagent/as-element [counter/counter opts value]))))
|
(h/is-truthy)))
|
||||||
|
|
||||||
(js/global.test "default render of counter component"
|
(h/test "renders counter with a string value"
|
||||||
(fn []
|
(h/render [counter/counter {} "1"])
|
||||||
(render-counter)
|
(-> (h/expect (h/get-by-text "1"))
|
||||||
(-> (js/expect (rtl/screen.getByTestId "counter-component"))
|
(h/is-truthy)))
|
||||||
(.toBeTruthy))))
|
|
||||||
|
|
||||||
(js/global.test "renders counter with a string value"
|
(h/test "renders counter with an integer value"
|
||||||
(fn []
|
(h/render [counter/counter {} 1])
|
||||||
(render-counter {} "1")
|
(-> (h/expect (h/get-by-text "1"))
|
||||||
(-> (js/expect (rtl/screen.getByText "1"))
|
(h/is-truthy)))
|
||||||
(.toBeTruthy))))
|
|
||||||
|
|
||||||
(js/global.test "renders counter with an integer value"
|
(h/test "renders counter with max value 99+ by default"
|
||||||
(fn []
|
(h/render [counter/counter {} 100])
|
||||||
(render-counter {} 1)
|
(-> (h/expect (h/get-by-text "99+"))
|
||||||
(-> (js/expect (rtl/screen.getByText "1"))
|
(h/is-truthy)))
|
||||||
(.toBeTruthy))))
|
|
||||||
|
|
||||||
(js/global.test "renders counter with value 99+ when the value is greater than 99"
|
(h/test "renders counter with custom max value when set to 150"
|
||||||
(fn []
|
(h/render [counter/counter {:max-value 150} 151])
|
||||||
(render-counter {} "100")
|
(-> (h/expect (h/get-by-text "150+"))
|
||||||
(-> (js/expect (rtl/screen.getByText "99+"))
|
(h/is-truthy))))
|
||||||
(.toBeTruthy))))
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
(ns quo2.components.counter.counter
|
(ns quo2.components.counter.counter
|
||||||
(:require [quo2.components.markdown.text :as text]
|
(:require
|
||||||
|
[quo2.components.markdown.text :as text]
|
||||||
[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]
|
||||||
|
[utils.number :as utils-number]))
|
||||||
|
|
||||||
(def themes
|
(def themes
|
||||||
{:light {:default colors/primary-50
|
{:light {:default colors/primary-50
|
||||||
@ -19,9 +21,9 @@
|
|||||||
(get-in themes [(theme/get-theme) key]))
|
(get-in themes [(theme/get-theme) key]))
|
||||||
|
|
||||||
(defn counter
|
(defn counter
|
||||||
"type: default, secondary, grey, outline
|
[{:keys [type override-text-color override-bg-color style accessibility-label max-value]
|
||||||
value: integer"
|
:or {max-value 99}}
|
||||||
[{:keys [type override-text-color override-bg-color style accessibility-label]} value]
|
value]
|
||||||
(let [type (or type :default)
|
(let [type (or type :default)
|
||||||
text-color (or override-text-color
|
text-color (or override-text-color
|
||||||
(if (or
|
(if (or
|
||||||
@ -29,11 +31,9 @@
|
|||||||
(= type :default))
|
(= type :default))
|
||||||
colors/white
|
colors/white
|
||||||
colors/neutral-100))
|
colors/neutral-100))
|
||||||
value (if (integer? value)
|
value (utils-number/parse-int value)
|
||||||
value
|
label (if (> value max-value)
|
||||||
(js/parseInt value))
|
(str max-value "+")
|
||||||
label (if (> value 99)
|
|
||||||
"99+"
|
|
||||||
(str value))
|
(str value))
|
||||||
width (case (count label)
|
width (case (count label)
|
||||||
1 16
|
1 16
|
||||||
@ -59,7 +59,7 @@
|
|||||||
(or override-bg-color
|
(or override-bg-color
|
||||||
(get-color type)))
|
(get-color type)))
|
||||||
|
|
||||||
(> value 99)
|
(> value max-value)
|
||||||
(assoc :padding-left 0.5))}
|
(assoc :padding-left 0.5))}
|
||||||
[text/text
|
[text/text
|
||||||
{:weight :medium
|
{:weight :medium
|
||||||
|
@ -66,3 +66,7 @@
|
|||||||
(update :message #(when % (messages/<-rpc %)))
|
(update :message #(when % (messages/<-rpc %)))
|
||||||
(update :reply-message #(when % (messages/<-rpc %)))
|
(update :reply-message #(when % (messages/<-rpc %)))
|
||||||
(dissoc :chatId)))
|
(dissoc :chatId)))
|
||||||
|
|
||||||
|
(defn <-rpc-seen-state
|
||||||
|
[item]
|
||||||
|
(:hasSeen item))
|
||||||
|
@ -414,6 +414,7 @@
|
|||||||
(communities/fetch)
|
(communities/fetch)
|
||||||
(logging/set-log-level (:log-level multiaccount))
|
(logging/set-log-level (:log-level multiaccount))
|
||||||
(activity-center/notifications-fetch-pending-contact-requests)
|
(activity-center/notifications-fetch-pending-contact-requests)
|
||||||
|
(activity-center/update-seen-state)
|
||||||
(activity-center/notifications-fetch-unread-count))))
|
(activity-center/notifications-fetch-unread-count))))
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
^js invitations (.-invitations response-js)
|
^js invitations (.-invitations response-js)
|
||||||
^js removed-chats (.-removedChats response-js)
|
^js removed-chats (.-removedChats response-js)
|
||||||
^js activity-notifications (.-activityCenterNotifications response-js)
|
^js activity-notifications (.-activityCenterNotifications response-js)
|
||||||
|
^js activity-center-state (.-activityCenterState response-js)
|
||||||
^js pin-messages (.-pinMessages response-js)
|
^js pin-messages (.-pinMessages response-js)
|
||||||
^js removed-messages (.-removedMessages response-js)
|
^js removed-messages (.-removedMessages response-js)
|
||||||
^js visibility-status-updates (.-statusUpdates response-js)
|
^js visibility-status-updates (.-statusUpdates response-js)
|
||||||
@ -75,6 +76,15 @@
|
|||||||
(activity-center/show-toasts notifications)
|
(activity-center/show-toasts notifications)
|
||||||
(process-next response-js sync-handler)))
|
(process-next response-js sync-handler)))
|
||||||
|
|
||||||
|
(some? activity-center-state)
|
||||||
|
(let [seen? (-> activity-center-state
|
||||||
|
types/js->clj
|
||||||
|
data-store.activities/<-rpc-seen-state)]
|
||||||
|
(js-delete response-js "activityCenterState")
|
||||||
|
(rf/merge cofx
|
||||||
|
(activity-center/reconcile-seen-state seen?)
|
||||||
|
(process-next response-js sync-handler)))
|
||||||
|
|
||||||
(seq installations)
|
(seq installations)
|
||||||
(let [installations-clj (types/js->clj installations)]
|
(let [installations-clj (types/js->clj installations)]
|
||||||
(js-delete response-js "installations")
|
(js-delete response-js "installations")
|
||||||
|
@ -13,3 +13,34 @@
|
|||||||
:margin-right 6
|
:margin-right 6
|
||||||
:weight :semi-bold
|
:weight :semi-bold
|
||||||
:size :heading-1})
|
:size :heading-1})
|
||||||
|
|
||||||
|
(defn unread-indicator
|
||||||
|
[unread-count max-value]
|
||||||
|
(let [right-offset (cond
|
||||||
|
(> unread-count max-value)
|
||||||
|
-14
|
||||||
|
|
||||||
|
;; Greater than 9 means we'll need 2 digits to represent
|
||||||
|
;; the text.
|
||||||
|
(> unread-count 9)
|
||||||
|
-10
|
||||||
|
|
||||||
|
:else -6)]
|
||||||
|
{:position :absolute
|
||||||
|
:top -6
|
||||||
|
:right right-offset
|
||||||
|
:z-index 4}))
|
||||||
|
|
||||||
|
(def left-section
|
||||||
|
{:position :absolute
|
||||||
|
:left 20
|
||||||
|
:top 12})
|
||||||
|
|
||||||
|
(def right-section
|
||||||
|
{:position :absolute
|
||||||
|
:right 20
|
||||||
|
:top 12
|
||||||
|
:flex-direction :row})
|
||||||
|
|
||||||
|
(def top-nav-container
|
||||||
|
{:height 56})
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
(ns status-im2.common.home.view
|
(ns status-im2.common.home.view
|
||||||
(:require [quo2.core :as quo]
|
(: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]
|
||||||
[react-native.hole-view :as hole-view]
|
|
||||||
[status-im2.common.home.style :as style]
|
[status-im2.common.home.style :as style]
|
||||||
[status-im2.common.plus-button.view :as components.plus-button]
|
[status-im2.common.plus-button.view :as plus-button]
|
||||||
|
[status-im2.constants :as constants]
|
||||||
[utils.re-frame :as rf]))
|
[utils.re-frame :as rf]))
|
||||||
|
|
||||||
(defn title-column
|
(defn title-column
|
||||||
@ -13,7 +14,7 @@
|
|||||||
[rn/view {:flex 1}
|
[rn/view {:flex 1}
|
||||||
[quo/text style/title-column-text
|
[quo/text style/title-column-text
|
||||||
label]]
|
label]]
|
||||||
[components.plus-button/plus-button
|
[plus-button/plus-button
|
||||||
{:on-press handler
|
{:on-press handler
|
||||||
:accessibility-label accessibility-label}]])
|
:accessibility-label accessibility-label}]])
|
||||||
|
|
||||||
@ -30,84 +31,65 @@
|
|||||||
:override-background-color (when (and dark? default?)
|
:override-background-color (when (and dark? default?)
|
||||||
colors/neutral-90)}))
|
colors/neutral-90)}))
|
||||||
|
|
||||||
(defn- base-button
|
(defn- unread-indicator
|
||||||
[icon on-press accessibility-label button-common-props]
|
[]
|
||||||
[quo/button
|
(let [unread-count (rf/sub [:activity-center/unread-count])
|
||||||
(merge
|
indicator (rf/sub [:activity-center/unread-indicator])
|
||||||
{:on-press on-press
|
unread-type (case indicator
|
||||||
:accessibility-label accessibility-label}
|
:unread-indicator/seen :grey
|
||||||
button-common-props)
|
:unread-indicator/new :default
|
||||||
icon])
|
nil)]
|
||||||
|
(when (pos? unread-count)
|
||||||
|
[quo/counter
|
||||||
|
{:accessibility-label :activity-center-unread-count
|
||||||
|
:type unread-type
|
||||||
|
:style (style/unread-indicator unread-count
|
||||||
|
constants/activity-center-max-unread-count)}
|
||||||
|
unread-count])))
|
||||||
|
|
||||||
(defn top-nav
|
(defn- left-section
|
||||||
"[top-nav opts]
|
[{:keys [avatar]}]
|
||||||
opts
|
|
||||||
{:type :default/:blurred/:shell
|
|
||||||
:style override-style
|
|
||||||
:avatar user-avatar}
|
|
||||||
"
|
|
||||||
[{:keys [type style avatar search?] :or {type :default}}]
|
|
||||||
(let [button-common-props (get-button-common-props type)
|
|
||||||
notif-count (rf/sub [:activity-center/unread-count])
|
|
||||||
new-notifications? (pos? notif-count)
|
|
||||||
notification-indicator :unread-dot
|
|
||||||
counter-label "0"]
|
|
||||||
[rn/view {:style (assoc style :height 56)}
|
|
||||||
;; Left Section
|
|
||||||
[rn/touchable-without-feedback {:on-press #(rf/dispatch [:navigate-to :my-profile])}
|
[rn/touchable-without-feedback {:on-press #(rf/dispatch [:navigate-to :my-profile])}
|
||||||
[rn/view
|
[rn/view
|
||||||
{:accessibility-label :open-profile
|
{:accessibility-label :open-profile
|
||||||
:style {:position :absolute
|
:style style/left-section}
|
||||||
:left 20
|
|
||||||
:top 12}}
|
|
||||||
[quo/user-avatar
|
[quo/user-avatar
|
||||||
(merge
|
(merge {:status-indicator? true
|
||||||
{:status-indicator? true
|
|
||||||
:size :small}
|
:size :small}
|
||||||
avatar)]]]
|
avatar)]]])
|
||||||
;; Right Section
|
|
||||||
[rn/view
|
(defn- right-section
|
||||||
{:style {:position :absolute
|
[{:keys [button-type search?]}]
|
||||||
:right 20
|
(let [button-common-props (get-button-common-props button-type)]
|
||||||
:top 12
|
[rn/view {:style style/right-section}
|
||||||
:flex-direction :row}}
|
|
||||||
(when search?
|
(when search?
|
||||||
[base-button :i/search #() :open-search-button button-common-props])
|
[quo/button
|
||||||
[base-button :i/scan #() :open-scanner-button button-common-props]
|
(assoc button-common-props :accessibility-label :open-search-button)
|
||||||
[base-button :i/qr-code #() :show-qr-button button-common-props]
|
:i/search])
|
||||||
[rn/view ;; Keep view instead of "[:<>" to make sure relative
|
[quo/button
|
||||||
;; position is calculated from this view instead of its parent
|
(assoc button-common-props :accessibility-label :open-scanner-button)
|
||||||
[hole-view/hole-view
|
:i/scan]
|
||||||
{:key new-notifications? ;; Key is required to force removal of holes
|
[quo/button
|
||||||
:holes (cond
|
(assoc button-common-props :accessibility-label :show-qr-button)
|
||||||
(not new-notifications?) ;; No new notifications, remove holes
|
:i/qr-code]
|
||||||
[]
|
|
||||||
|
|
||||||
(= notification-indicator :unread-dot)
|
|
||||||
[{:x 37 :y -3 :width 10 :height 10 :borderRadius 5}]
|
|
||||||
|
|
||||||
:else
|
|
||||||
[{:x 33 :y -7 :width 18 :height 18 :borderRadius 7}])}
|
|
||||||
[base-button :i/activity-center #(rf/dispatch [:activity-center/open])
|
|
||||||
:open-activity-center-button button-common-props]]
|
|
||||||
(when new-notifications?
|
|
||||||
(if (= notification-indicator :counter)
|
|
||||||
[quo/counter
|
|
||||||
{:accessibility-label :notifications-unread-badge
|
|
||||||
:outline false
|
|
||||||
:override-text-color colors/white
|
|
||||||
:override-bg-color colors/primary-50
|
|
||||||
:style {:position :absolute
|
|
||||||
:left 34
|
|
||||||
:top -6}}
|
|
||||||
counter-label]
|
|
||||||
[rn/view
|
[rn/view
|
||||||
{:accessible true
|
[unread-indicator]
|
||||||
:accessibility-label :notifications-unread-badge
|
[quo/button
|
||||||
:style {:width 8
|
(merge button-common-props
|
||||||
:height 8
|
{:accessibility-label :open-activity-center-button
|
||||||
:border-radius 4
|
:on-press #(rf/dispatch [:activity-center/open])})
|
||||||
:top -2
|
:i/activity-center]]]))
|
||||||
:left 38
|
|
||||||
:position :absolute
|
(defn top-nav
|
||||||
:background-color colors/primary-50}}]))]]]))
|
"[top-nav props]
|
||||||
|
props
|
||||||
|
{:type quo/button types
|
||||||
|
:style override-style
|
||||||
|
:avatar user-avatar
|
||||||
|
:search? When non-nil, show search button}
|
||||||
|
"
|
||||||
|
[{:keys [type style avatar search?]
|
||||||
|
:or {type :default}}]
|
||||||
|
[rn/view {:style (merge style/top-nav-container style)}
|
||||||
|
[left-section {:avatar avatar}]
|
||||||
|
[right-section {:button-type type :search? search?}]])
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
(def ^:const activity-center-membership-status-declined 3)
|
(def ^:const activity-center-membership-status-declined 3)
|
||||||
|
|
||||||
(def ^:const activity-center-mark-all-as-read-undo-time-limit-ms 4000)
|
(def ^:const activity-center-mark-all-as-read-undo-time-limit-ms 4000)
|
||||||
|
(def ^:const activity-center-max-unread-count 99)
|
||||||
|
|
||||||
(def ^:const emoji-reaction-love 1)
|
(def ^:const emoji-reaction-love 1)
|
||||||
(def ^:const emoji-reaction-thumbs-up 2)
|
(def ^:const emoji-reaction-thumbs-up 2)
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
[taoensso.timbre :as log]
|
[taoensso.timbre :as log]
|
||||||
[utils.collection :as collection]
|
[utils.collection :as collection]
|
||||||
[utils.i18n :as i18n]
|
[utils.i18n :as i18n]
|
||||||
[utils.re-frame :as rf]
|
[utils.re-frame :as rf]))
|
||||||
[status-im2.navigation.events :as navigation]))
|
|
||||||
|
|
||||||
(def defaults
|
(def defaults
|
||||||
{:filter-status :unread
|
{:filter-status :unread
|
||||||
@ -24,15 +23,18 @@
|
|||||||
|
|
||||||
(rf/defn open-activity-center
|
(rf/defn open-activity-center
|
||||||
{:events [:activity-center/open]}
|
{:events [:activity-center/open]}
|
||||||
[{:keys [db] :as cofx} {:keys [filter-type filter-status]}]
|
[{:keys [db]} {:keys [filter-type filter-status]}]
|
||||||
(rf/merge cofx
|
|
||||||
{:db (cond-> db
|
{:db (cond-> db
|
||||||
filter-status
|
filter-status
|
||||||
(assoc-in [:activity-center :filter :status] filter-status)
|
(assoc-in [:activity-center :filter :status] filter-status)
|
||||||
|
|
||||||
filter-type
|
filter-type
|
||||||
(assoc-in [:activity-center :filter :type] filter-type))}
|
(assoc-in [:activity-center :filter :type] filter-type))
|
||||||
(navigation/open-modal :activity-center {})))
|
:dispatch [:open-modal :activity-center {}]
|
||||||
|
;; We delay marking as seen so that the user doesn't see the unread bell icon
|
||||||
|
;; change while the Activity Center modal is opening.
|
||||||
|
:dispatch-later [{:ms 1000
|
||||||
|
:dispatch [:activity-center/mark-as-seen]}]})
|
||||||
|
|
||||||
;;;; Misc
|
;;;; Misc
|
||||||
|
|
||||||
@ -48,7 +50,7 @@
|
|||||||
(filter #(= notification-id (:id %)))
|
(filter #(= notification-id (:id %)))
|
||||||
first))
|
first))
|
||||||
|
|
||||||
;;;; Notification reconciliation
|
;;;; Reconciliation
|
||||||
|
|
||||||
(defn- update-notifications
|
(defn- update-notifications
|
||||||
[db-notifications new-notifications {filter-type :type filter-status :status}]
|
[db-notifications new-notifications {filter-type :type filter-status :status}]
|
||||||
@ -97,6 +99,11 @@
|
|||||||
(remove #(activities/pending-contact-request? contact-id %)
|
(remove #(activities/pending-contact-request? contact-id %)
|
||||||
notifications)))})
|
notifications)))})
|
||||||
|
|
||||||
|
(rf/defn reconcile-seen-state
|
||||||
|
{:events [:activity-center/reconcile-seen-state]}
|
||||||
|
[{:keys [db]} seen?]
|
||||||
|
{:db (assoc-in db [:activity-center :seen?] seen?)})
|
||||||
|
|
||||||
;;;; Status changes (read/dismissed/deleted)
|
;;;; Status changes (read/dismissed/deleted)
|
||||||
|
|
||||||
(rf/defn mark-as-read
|
(rf/defn mark-as-read
|
||||||
@ -451,6 +458,50 @@
|
|||||||
|
|
||||||
;;;; Unread counters
|
;;;; Unread counters
|
||||||
|
|
||||||
|
(rf/defn update-seen-state
|
||||||
|
{:events [:activity-center/update-seen-state]}
|
||||||
|
[_]
|
||||||
|
{:json-rpc/call
|
||||||
|
[{:method "wakuext_hasUnseenActivityCenterNotifications"
|
||||||
|
:params []
|
||||||
|
:on-success #(rf/dispatch [:activity-center/update-seen-state-success %])
|
||||||
|
:on-error #(rf/dispatch [:activity-center/update-seen-state-error %])}]})
|
||||||
|
|
||||||
|
(rf/defn update-seen-state-success
|
||||||
|
{:events [:activity-center/update-seen-state-success]}
|
||||||
|
[{:keys [db]} unseen?]
|
||||||
|
{:db (assoc-in db [:activity-center :seen?] (not unseen?))})
|
||||||
|
|
||||||
|
(rf/defn update-seen-state-error
|
||||||
|
{:events [:activity-center/update-seen-state-error]}
|
||||||
|
[_ error]
|
||||||
|
(log/error "Failed to update Activity Center seen state"
|
||||||
|
{:error error
|
||||||
|
:event :activity-center/update-seen-state}))
|
||||||
|
|
||||||
|
(rf/defn mark-as-seen
|
||||||
|
{:events [:activity-center/mark-as-seen]}
|
||||||
|
[_]
|
||||||
|
{:json-rpc/call
|
||||||
|
[{:method "wakuext_markAsSeenActivityCenterNotifications"
|
||||||
|
:params []
|
||||||
|
:on-success #(rf/dispatch [:activity-center/mark-as-seen-success %])
|
||||||
|
:on-error #(rf/dispatch [:activity-center/mark-as-seen-error %])}]})
|
||||||
|
|
||||||
|
(rf/defn mark-as-seen-success
|
||||||
|
{:events [:activity-center/mark-as-seen-success]}
|
||||||
|
[{:keys [db]} response]
|
||||||
|
{:db (assoc-in db
|
||||||
|
[:activity-center :seen?]
|
||||||
|
(get-in response [:activityCenterState :hasSeen]))})
|
||||||
|
|
||||||
|
(rf/defn mark-as-seen-error
|
||||||
|
{:events [:activity-center/mark-as-seen-error]}
|
||||||
|
[_ error]
|
||||||
|
(log/error "Failed to mark Activity Center as seen"
|
||||||
|
{:error error
|
||||||
|
:event :activity-center/mark-as-seen}))
|
||||||
|
|
||||||
(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]}
|
||||||
[_]
|
[_]
|
||||||
|
@ -34,6 +34,22 @@
|
|||||||
vals
|
vals
|
||||||
(reduce + 0))))
|
(reduce + 0))))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:activity-center/seen?
|
||||||
|
:<- [:activity-center]
|
||||||
|
(fn [activity-center]
|
||||||
|
(:seen? activity-center)))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:activity-center/unread-indicator
|
||||||
|
:<- [:activity-center/seen?]
|
||||||
|
:<- [:activity-center/unread-count]
|
||||||
|
(fn [[seen? unread-count]]
|
||||||
|
(cond
|
||||||
|
(zero? unread-count) :unread-indicator/none
|
||||||
|
seen? :unread-indicator/seen
|
||||||
|
:else :unread-indicator/new)))
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
:activity-center/mark-all-as-read-undoable-till
|
:activity-center/mark-all-as-read-undoable-till
|
||||||
:<- [:activity-center]
|
:<- [:activity-center]
|
||||||
|
@ -63,3 +63,25 @@
|
|||||||
types/admin 7})
|
types/admin 7})
|
||||||
|
|
||||||
(is (= 28 (rf/sub [sub-name]))))
|
(is (= 28 (rf/sub [sub-name]))))
|
||||||
|
|
||||||
|
(h/deftest-sub :activity-center/unread-indicator
|
||||||
|
[sub-name]
|
||||||
|
(testing "not seen and no unread notifications"
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 0})
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :seen?] false)
|
||||||
|
(is (= :unread-indicator/none (rf/sub [sub-name]))))
|
||||||
|
|
||||||
|
(testing "not seen and one or more unread notifications"
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 1})
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :seen?] false)
|
||||||
|
(is (= :unread-indicator/new (rf/sub [sub-name]))))
|
||||||
|
|
||||||
|
(testing "seen and no unread notifications"
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 0})
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :seen?] true)
|
||||||
|
(is (= :unread-indicator/none (rf/sub [sub-name]))))
|
||||||
|
|
||||||
|
(testing "seen and one or more unread notifications"
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :unread-counts-by-type] {types/one-to-one-chat 1})
|
||||||
|
(swap! rf-db/app-db assoc-in [:activity-center :seen?] true)
|
||||||
|
(is (= :unread-indicator/seen (rf/sub [sub-name])))))
|
||||||
|
@ -13,3 +13,13 @@
|
|||||||
(let [scale (Math/pow 10 decimal-places)]
|
(let [scale (Math/pow 10 decimal-places)]
|
||||||
(/ (Math/round (* n scale))
|
(/ (Math/round (* n scale))
|
||||||
scale)))
|
scale)))
|
||||||
|
|
||||||
|
(defn parse-int
|
||||||
|
"Parses `n` as an integer. Defaults to zero or `default` instead of NaN."
|
||||||
|
([n]
|
||||||
|
(parse-int n 0))
|
||||||
|
([n default]
|
||||||
|
(let [maybe-int (js/parseInt n 10)]
|
||||||
|
(if (integer? maybe-int)
|
||||||
|
maybe-int
|
||||||
|
default))))
|
||||||
|
17
src/utils/number_test.cljs
Normal file
17
src/utils/number_test.cljs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
(ns utils.number-test
|
||||||
|
(:require [cljs.test :refer [deftest is testing]]
|
||||||
|
[utils.number :as utils-number]))
|
||||||
|
|
||||||
|
(deftest parse-int
|
||||||
|
(testing "defaults to zero"
|
||||||
|
(is (= 0 (utils-number/parse-int nil))))
|
||||||
|
|
||||||
|
(testing "accepts any other default value"
|
||||||
|
(is (= 3 (utils-number/parse-int "" 3)))
|
||||||
|
(is (= :invalid-int (utils-number/parse-int "" :invalid-int))))
|
||||||
|
|
||||||
|
(testing "valid numbers"
|
||||||
|
(is (= -6 (utils-number/parse-int "-6a" 0)))
|
||||||
|
(is (= 6 (utils-number/parse-int "6" 0)))
|
||||||
|
(is (= 6 (utils-number/parse-int "6.99" 0)))
|
||||||
|
(is (= -6 (utils-number/parse-int "-6" 0)))))
|
@ -197,7 +197,7 @@ class HomeView(BaseView):
|
|||||||
|
|
||||||
# Notification centre
|
# Notification centre
|
||||||
self.notifications_button = Button(self.driver, accessibility_id="notifications-button")
|
self.notifications_button = Button(self.driver, accessibility_id="notifications-button")
|
||||||
self.notifications_unread_badge = BaseElement(self.driver, accessibility_id="notifications-unread-badge")
|
self.notifications_unread_badge = BaseElement(self.driver, accessibility_id="activity-center-unread-count")
|
||||||
self.open_activity_center_button = Button(self.driver, accessibility_id="open-activity-center-button")
|
self.open_activity_center_button = Button(self.driver, accessibility_id="open-activity-center-button")
|
||||||
self.close_activity_centre = Button(self.driver, accessibility_id="close-activity-center")
|
self.close_activity_centre = Button(self.driver, accessibility_id="close-activity-center")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user