Redesign and simplify Activity Center app db (#15216)

Fixes #15215. Redesign the app db state for the Activity Center. Please, see the issue being fixed for more details about the problems being solved.

TL;DR: There's a lot less state to keep track and reconcile and way less nesting in the app db because we're only managing the state of the *current tab*, not all tabs.

Additionally:

- [x] While updating unit tests, found a bug on the sorting notifications' logic.
- [x] While updating unit tests, found a bug where notifications that are not of the type *contact request* were being removed from the app db.
- [x] Fixed regression where pressing on a notification would not open the chat.
- [x] Hardened unit tests.

#### Platforms

- Android
- iOS

##### Non-functional

- Less memory consumption.
- Faster reconciliation of notification coming from signals and from synchronous responses from RPC calls.
This commit is contained in:
Icaro Motta 2023-03-03 09:17:35 -03:00 committed by GitHub
parent 25d44b11f1
commit 52b87bab3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 377 additions and 553 deletions

View File

@ -1,13 +1,18 @@
(ns status-im.data-store.activities
(:require [clojure.set :as set]
[status-im2.constants :as constants]
[status-im.data-store.messages :as messages]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification-types :as notification-types]))
(defn mark-notifications-as-read
[notifications]
(map #(assoc % :read true) notifications))
(defn pending-contact-request?
[contact-id {:keys [type author]}]
(and (= type notification-types/contact-request)
(= contact-id author)))
(defn- rpc->type
[{:keys [type name] :as chat}]
(case type

View File

@ -97,3 +97,13 @@
(assoc :type notification-types/one-to-one-chat)
store/<-rpc
(select-keys [:name :chat-type :chat-name :public? :group-chat]))))))
(deftest remove-pending-contact-request-test
(is (true? (store/pending-contact-request?
"contact-id"
{:type notification-types/contact-request
:author "contact-id"})))
(is (false? (store/pending-contact-request?
"contact-id"
{:type notification-types/contact-request
:author "contactzzzz"}))))

View File

@ -409,7 +409,7 @@
(get-node-config)
(communities/fetch)
(logging/set-log-level (:log-level multiaccount))
(activity-center/notifications-fetch-unread-contact-requests)
(activity-center/notifications-fetch-pending-contact-requests)
(activity-center/notifications-fetch-unread-count))))
(re-frame/reg-fx

View File

@ -1,15 +1,16 @@
(ns status-im2.contexts.activity-center.events
(:require [status-im.data-store.activities :as data-store.activities]
(:require [quo2.foundations.colors :as colors]
[status-im.data-store.activities :as activities]
[status-im.data-store.chats :as data-store.chats]
[status-im2.contexts.activity-center.notification-types :as types]
[status-im2.contexts.chat.events :as chat.events]
[status-im2.common.toasts.events :as toasts]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification-types :as types]
status-im2.contexts.activity-center.notification.contact-requests.events
[status-im2.contexts.chat.events :as chat.events]
[taoensso.timbre :as log]
[utils.re-frame :as rf]
[utils.collection :as collection]
[utils.i18n :as i18n]
[quo2.foundations.colors :as colors]
[status-im2.constants :as constants]))
[utils.re-frame :as rf]))
(def defaults
{:filter-status :unread
@ -48,127 +49,58 @@
;;;; Notification reconciliation
(defn- notification-type->filter-type
[type]
(if (some types/membership [type])
types/membership
type))
(defn- update-notifications
"Insert `new-notifications` in `db-notifications`.
Although correct, this is a naive implementation for reconciling notifications
because for every notification in `new-notifications`, linear scans will be
performed to remove it and sorting will be performed for every new insertion.
If the number of existing notifications cached in the app db becomes
~excessively~ big, this implementation will probably need to be revisited."
[db-notifications new-notifications]
(reduce (fn [acc {:keys [id read] :as notification}]
(let [filter-type (notification-type->filter-type (:type notification))
remove-notification (fn [data]
(remove #(= id (:id %)) data))
insert-and-sort (fn [data]
(->> notification
(conj data)
(sort-by (juxt :timestamp :id))
reverse))]
(as-> acc $
(update-in $ [filter-type :all :data] remove-notification)
(update-in $ [types/no-type :all :data] remove-notification)
(update-in $ [filter-type :unread :data] remove-notification)
(update-in $ [types/no-type :unread :data] remove-notification)
(if (:deleted notification)
$
(cond-> (-> $
(update-in [filter-type :all :data] insert-and-sort)
(update-in [types/no-type :all :data] insert-and-sort))
(not read) (update-in [filter-type :unread :data] insert-and-sort)
(not read) (update-in [types/no-type :unread :data] insert-and-sort))))))
db-notifications
new-notifications))
[db-notifications new-notifications {filter-type :type filter-status :status}]
(->> new-notifications
(reduce (fn [acc {:keys [id type deleted read] :as notification}]
(if (or deleted
(and (= :unread filter-status) read)
(and (set? filter-type)
(not (contains? filter-type type)))
(and (not (set? filter-type))
(not= filter-type types/no-type)
(not= filter-type type)))
(dissoc acc id)
(assoc acc id notification)))
(collection/index-by :id db-notifications))
(vals)
(sort-by (juxt :timestamp :id)
#(compare %2 %1))))
(rf/defn notifications-reconcile
{:events [:activity-center.notifications/reconcile]}
[{:keys [db]} new-notifications]
(when (seq new-notifications)
{:db (update-in db
[:activity-center :notifications]
update-notifications
new-notifications)
:dispatch [:activity-center.notifications/fetch-unread-count]}))
(rf/defn show-toasts
{:events [:activity-center.notifications/show-toasts]}
[{:keys [db]} new-notifications]
(let [my-public-key (get-in db [:multiaccount :public-key])]
(reduce (fn [cofx {:keys [author type accepted dismissed message name] :as x}]
(cond
(and (not= author my-public-key)
(= type types/contact-request)
(not accepted)
(not dismissed))
(toasts/upsert cofx
{:icon :placeholder
:icon-color colors/primary-50-opa-40
:title (i18n/label :t/contact-request-sent-toast
{:name name})
:text (get-in message [:content :text])})
(and (= author my-public-key) ;; we show it for user who sent the request
(= type types/contact-request)
accepted
(not dismissed))
(toasts/upsert cofx
{:icon :placeholder
:icon-color colors/primary-50-opa-40
:title (i18n/label :t/contact-request-accepted-toast
{:name (:alias message)})})
:else
cofx))
{:db db}
new-notifications)))
{:db (update-in db
[:activity-center :notifications]
update-notifications
new-notifications
(get-in db [:activity-center :filter]))
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
[:activity-center.notifications/fetch-pending-contact-requests]]}))
(rf/defn notifications-reconcile-from-response
{:events [:activity-center/reconcile-notifications-from-response]}
[cofx response]
(->> response
:activityCenterNotifications
(map data-store.activities/<-rpc)
(map activities/<-rpc)
(notifications-reconcile cofx)))
(defn- remove-pending-contact-request
[notifications contact-id]
(remove #(= contact-id (:author %))
notifications))
(rf/defn notifications-remove-pending-contact-request
{:events [:activity-center/remove-pending-contact-request]}
[{:keys [db]} contact-id]
{:db (-> db
(update-in [:activity-center :notifications types/no-type :all :data]
remove-pending-contact-request
contact-id)
(update-in [:activity-center :notifications types/no-type :unread :data]
remove-pending-contact-request
contact-id)
(update-in [:activity-center :notifications types/contact-request :all :data]
remove-pending-contact-request
contact-id)
(update-in [:activity-center :notifications types/contact-request :unread :data]
remove-pending-contact-request
contact-id))})
{:db (update-in db
[:activity-center :notifications]
(fn [notifications]
(remove #(activities/pending-contact-request? contact-id %)
notifications)))})
;;;; Status changes (read/dismissed/deleted)
(defn- get-notification
[db notification-id]
(->> (get-in db
[:activity-center
:notifications
(get-in db [:activity-center :filter :type])
(get-in db [:activity-center :filter :status])
:data])
(->> (get-in db [:activity-center :notifications])
(filter #(= notification-id (:id %)))
first))
@ -237,20 +169,23 @@
{:db (-> db
(update-in [:activity-center :notifications]
update-notifications
notifications)
notifications
(get-in db [:activity-center :filter]))
(update :activity-center dissoc :mark-all-as-read-undoable-till))})
(rf/defn mark-all-as-read-locally
{:events [:activity-center.notifications/mark-all-as-read-locally]}
[{:keys [db now]} get-toast-ui-props]
(let [unread-notifications (get-in db [:activity-center :notifications types/no-type :unread :data])
(let [unread-notifications (filter #(not (:read %))
(get-in db [:activity-center :notifications]))
undo-time-limit-ms constants/activity-center-mark-all-as-read-undo-time-limit-ms
undoable-till (+ now undo-time-limit-ms)]
{:db (-> db
(update-in [:activity-center :notifications]
update-notifications
(data-store.activities/mark-notifications-as-read
unread-notifications))
(activities/mark-notifications-as-read
unread-notifications)
(get-in db [:activity-center :filter]))
(assoc-in [:activity-center :mark-all-as-read-undoable-till]
undoable-till))
:dispatch [:toasts/upsert
@ -379,7 +314,7 @@
(def start-or-end-cursor
"")
(defn- valid-cursor?
(defn- fetch-more?
[cursor]
(and (some? cursor)
(not= cursor start-or-end-cursor)))
@ -411,12 +346,10 @@
(rf/defn notifications-fetch
[{:keys [db]} {:keys [cursor per-page filter-type filter-status reset-data?]}]
(when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?])
(when-not (get-in db [:activity-center :loading?])
(let [per-page (or per-page (defaults :notifications-per-page))
accepted? true]
{:db (assoc-in db
[:activity-center :notifications filter-type filter-status :loading?]
true)
{:db (assoc-in db [:activity-center :loading?] true)
:json-rpc/call [{:method "wakuext_activityCenterNotificationsBy"
:params [cursor
per-page
@ -424,7 +357,7 @@
(status filter-status)
accepted?]
:on-success #(rf/dispatch [:activity-center.notifications/fetch-success
filter-type filter-status reset-data? %])
reset-data? %])
:on-error #(rf/dispatch [:activity-center.notifications/fetch-error
filter-type filter-status %])}]})))
@ -452,8 +385,8 @@
{:events [:activity-center.notifications/fetch-next-page]}
[{:keys [db] :as cofx}]
(let [{:keys [type status]} (get-in db [:activity-center :filter])
{:keys [cursor]} (get-in db [:activity-center :notifications type status])]
(when (valid-cursor? cursor)
cursor (get-in db [:activity-center :cursor])]
(when (fetch-more? cursor)
(notifications-fetch cofx
{:cursor cursor
:filter-type type
@ -462,36 +395,59 @@
(rf/defn notifications-fetch-success
{:events [:activity-center.notifications/fetch-success]}
[{:keys [db]}
filter-type
filter-status
reset-data?
{:keys [cursor notifications]}]
(let [processed (map data-store.activities/<-rpc notifications)]
[{:keys [db]} reset-data? {:keys [cursor notifications]}]
(let [processed (map activities/<-rpc notifications)]
{:db (-> db
(assoc-in [:activity-center :notifications filter-type filter-status :cursor] cursor)
(update-in [:activity-center :notifications filter-type filter-status] dissoc :loading?)
(update-in [:activity-center :notifications filter-type filter-status :data]
(assoc-in [:activity-center :cursor] cursor)
(update :activity-center dissoc :loading?)
(update-in [:activity-center :notifications]
(if reset-data?
(constantly processed)
#(concat %1 processed))))}))
#(concat % processed))))}))
(rf/defn notifications-fetch-unread-contact-requests
(rf/defn notifications-fetch-pending-contact-requests
"Unread contact requests are, in practical terms, the same as pending contact
requests in the new Activity Center, because pending contact requests are
always marked as unread, and once the user declines/accepts the request, they
are marked as read.
requests in the Activity Center, because pending contact requests are always
marked as unread in status-go, and once the user declines/accepts the request,
they are marked as read.
If this relationship ever changes, we will probably need to change the backend
to explicitly support fetching notifications for 'pending' contact requests."
{:events [:activity-center.notifications/fetch-unread-contact-requests]}
[cofx]
(notifications-fetch cofx
{:cursor start-or-end-cursor
:filter-status :unread
:filter-type types/contact-request
:per-page 20
:reset-data? true}))
{:events [:activity-center.notifications/fetch-pending-contact-requests]}
[{:keys [db]}]
(let [accepted? true]
{:db (assoc-in db [:activity-center :loading?] true)
:json-rpc/call
[{:method "wakuext_activityCenterNotificationsBy"
:params [start-or-end-cursor
20
[types/contact-request]
(status :unread)
accepted?]
:on-success #(rf/dispatch [:activity-center.notifications/fetch-pending-contact-requests-success
%])
:on-error #(rf/dispatch [:activity-center.notifications/fetch-error
types/contact-request :unread %])}]}))
(rf/defn notifications-fetch-pending-contact-requests-success
{:events [:activity-center.notifications/fetch-pending-contact-requests-success]}
[{:keys [db]} {:keys [notifications]}]
{:db (-> db
(update :activity-center dissoc :loading?)
(assoc-in [:activity-center :contact-requests]
(->> notifications
(map activities/<-rpc)
(filter (fn [notification]
(= constants/contact-request-message-state-pending
(get-in notification [:message :contact-request-state])))))))})
(rf/defn notifications-fetch-error
{:events [:activity-center.notifications/fetch-error]}
[{:keys [db]} error]
(log/warn "Failed to load Activity Center notifications" error)
{:db (update db :activity-center dissoc :loading?)})
;;;; Unread counters
(rf/defn notifications-fetch-unread-count
{:events [:activity-center.notifications/fetch-unread-count]}
@ -521,8 +477,36 @@
(log/error "Failed to fetch count of notifications" {:error error})
nil)
(rf/defn notifications-fetch-error
{:events [:activity-center.notifications/fetch-error]}
[{:keys [db]} filter-type filter-status error]
(log/warn "Failed to load Activity Center notifications" error)
{:db (update-in db [:activity-center :notifications filter-type filter-status] dissoc :loading?)})
;;;; Toasts
(rf/defn show-toasts
{:events [:activity-center.notifications/show-toasts]}
[{:keys [db]} new-notifications]
(let [my-public-key (get-in db [:multiaccount :public-key])]
(reduce (fn [cofx {:keys [author type accepted dismissed message name] :as x}]
(cond
(and (not= author my-public-key)
(= type types/contact-request)
(not accepted)
(not dismissed))
(toasts/upsert cofx
{:icon :placeholder
:icon-color colors/primary-50-opa-40
:title (i18n/label :t/contact-request-sent-toast
{:name name})
:text (get-in message [:content :text])})
(and (= author my-public-key) ;; we show it for user who sent the request
(= type types/contact-request)
accepted
(not dismissed))
(toasts/upsert cofx
{:icon :placeholder
:icon-color colors/primary-50-opa-40
:title (i18n/label :t/contact-request-accepted-toast
{:name (:alias message)})})
:else
cofx))
{:db db}
new-notifications)))

View File

@ -3,7 +3,6 @@
[status-im2.constants :as constants]
status-im.events
[test-helpers.unit :as h]
[status-im2.contexts.activity-center.events :as activity-center]
[status-im2.contexts.activity-center.notification-types :as types]
[utils.re-frame :as rf]))
@ -53,15 +52,13 @@
(testing "opens the activity center without custom filters"
(h/run-test-sync
(let [spy-queue (atom [])
existing-filters {:status :all}]
(let [spy-queue (atom [])]
(setup)
(h/spy-fx spy-queue :show-popover)
(rf/dispatch [:test/assoc-in [:activity-center :filter] existing-filters])
(rf/dispatch [:activity-center/open])
(is (= existing-filters
(is (= {:status :unread :type types/no-type}
(get-in (h/db) [:activity-center :filter])))
(is (= [{:id :show-popover :args nil}]
@spy-queue))))))
@ -72,16 +69,10 @@
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue :json-rpc/call)
(let [notifications {types/one-to-one-chat
{:all {:cursor ""
:data [{:id notification-id
:read false
:type types/one-to-one-chat}]}
:unread {:cursor "" :data []}}}]
(rf/dispatch [:test/assoc-in [:activity-center]
{:notifications notifications
:filter {:type types/one-to-one-chat
:status :all}}])
(let [notifications [{:id notification-id
:read false
:type types/one-to-one-chat}]]
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
(rf/dispatch [:activity-center.notifications/mark-as-read "0x666"])
@ -98,30 +89,15 @@
new-notif-2 (assoc notif-2 :read true)]
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly nil))
(rf/dispatch [:test/assoc-in [:activity-center]
{:notifications {types/one-to-one-chat
{:all {:cursor "" :data [notif-3 notif-2 notif-1]}
:unread {:cursor "" :data [notif-3 notif-2]}}}
:filter {:type types/one-to-one-chat
:status :unread}}])
{:filter {:status :all :type types/no-type}
:notifications [notif-3 notif-2 notif-1]}])
(rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-2)])
(is (= {types/one-to-one-chat
{:all {:cursor "" :data [notif-3 new-notif-2 notif-1]}
:unread {:cursor "" :data [notif-3]}}
types/no-type
{:all {:data [new-notif-2]}
:unread {:data []}}}
(is (= [notif-3 new-notif-2 notif-1]
(get-in (h/db) [:activity-center :notifications])))
(rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-3)])
(is (= {types/one-to-one-chat
{:all {:cursor "" :data [new-notif-3 new-notif-2 notif-1]}
:unread {:cursor "" :data []}}
types/no-type
{:all {:data [new-notif-3 new-notif-2]}
:unread {:data []}}}
(is (= [new-notif-3 new-notif-2 notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "logs on failure"
@ -131,15 +107,10 @@
:action :notification/mark-as-read
:before-test (fn []
(rf/dispatch
[:test/assoc-in [:activity-center]
{:notifications {types/one-to-one-chat
{:all {:cursor ""
:data [{:id notification-id
:read false
:type types/one-to-one-chat}]}
:unread {:cursor "" :data []}}}
:filter {:type types/one-to-one-chat
:status :all}}]))})))
[:test/assoc-in [:activity-center :notifications]
[{:id notification-id
:read false
:type types/one-to-one-chat}]]))})))
;;;; Acceptance/dismissal
@ -147,27 +118,25 @@
(testing "marks notification as accepted and read, then reconciles"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :type types/private-group-chat}
notif-2 {:id "0x2" :type types/private-group-chat}
notif-3 {:id "0x3" :type types/admin}
notif-2-new (assoc notif-2 :accepted true :read true)]
(let [notif-1 {:id "0x1" :type types/private-group-chat}
notif-2 {:id "0x2" :type types/private-group-chat}
notif-2-accepted (assoc notif-2 :accepted true :read true)]
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2))
(rf/dispatch [:test/assoc-in [:activity-center]
{:notifications {types/membership
{:unread {:cursor "" :data [notif-2 notif-1]}}
types/admin
{:all {:cursor "" :data [notif-3]}}}
:filter {:type types/membership
:status :unread}}])
{:filter {:type types/no-type :status :all}
:notifications [notif-2 notif-1]}])
(rf/dispatch [:activity-center.notifications/accept (:id notif-2)])
(is (= {types/no-type {:all {:data [notif-2-new]}
:unread {:data []}}
types/membership {:all {:data [notif-2-new]}
:unread {:cursor "" :data [notif-1]}}
types/admin {:all {:cursor "" :data [notif-3]}}}
(is (= [notif-2-accepted notif-1]
(get-in (h/db) [:activity-center :notifications])))
;; Ignores accepted notification if the Unread filter is enabled because
;; accepted notifications are also marked as read in status-go.
(rf/dispatch [:test/assoc-in [:activity-center :filter]
{:filter {:type types/no-type :status :unread}}])
(rf/dispatch [:activity-center.notifications/accept (:id notif-2)])
(is (= [notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "logs on failure"
@ -184,24 +153,12 @@
notif-2 {:id "0x2" :type types/admin}
dismissed-notif-1 (assoc notif-1 :dismissed true)]
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2))
(rf/dispatch [:test/assoc-in [:activity-center]
{:notifications {types/no-type
{:all {:cursor "" :data [notif-2 notif-1]}}
types/membership
{:unread {:cursor "" :data [notif-1]}}}
:filter {:type types/membership
:status :unread}}])
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
[notif-2 notif-1]])
(rf/dispatch [:activity-center.notifications/dismiss (:id notif-1)])
(is (= {types/no-type
{:all {:cursor "" :data [notif-2 dismissed-notif-1]}
:unread {:data [dismissed-notif-1]}}
types/membership
{:all {:data [dismissed-notif-1]}
:unread {:cursor "" :data [dismissed-notif-1]}}}
(is (= [notif-2 dismissed-notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "logs on failure"
@ -254,36 +211,23 @@
:timestamp 1666647286000
:type types/contact-verification})
(defn test-contact-verification-event
[{:keys [event expected-rpc-call]}]
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks :json-rpc/call
:on-success
(constantly contact-verification-rpc-response))
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch event)
(is (= {types/no-type
{:all {:data [contact-verification-expected-notification]}
:unread {:data []}}
types/contact-verification
{:all {:data [contact-verification-expected-notification]}
:unread {:data []}}}
(get-in (h/db) [:activity-center :notifications])))
(is (= expected-rpc-call
(-> @spy-queue
(get-in [0 :args 0])
(select-keys [:method :params])))))))
(deftest contact-verification-decline-test
(testing "declines notification and reconciles"
(test-contact-verification-event
{:event [:activity-center.contact-verification/decline notification-id]
:expected-rpc-call {:method "wakuext_declineContactVerificationRequest"
:params [notification-id]}}))
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks :json-rpc/call
:on-success
(constantly contact-verification-rpc-response))
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/contact-verification :status :all}}])
(rf/dispatch [:activity-center.contact-verification/decline notification-id])
(is (= [contact-verification-expected-notification]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "logs on failure"
(test-log-on-failure
{:notification-id notification-id
@ -293,10 +237,21 @@
(deftest contact-verification-reply-test
(testing "sends reply and reconciles"
(let [reply "any answer"]
(test-contact-verification-event
{:event [:activity-center.contact-verification/reply notification-id reply]
:expected-rpc-call {:method "wakuext_acceptContactVerificationRequest"
:params [notification-id reply]}})))
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks :json-rpc/call
:on-success
(constantly contact-verification-rpc-response))
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/contact-verification :status :all}}])
(rf/dispatch [:activity-center.contact-verification/reply notification-id reply])
(is (= [contact-verification-expected-notification]
(get-in (h/db) [:activity-center :notifications])))))))
(testing "logs on failure"
(test-log-on-failure
{:notification-id notification-id
@ -304,11 +259,25 @@
:action :contact-verification/reply})))
(deftest contact-verification-mark-as-trusted-test
(testing "marks notification as trusted and reconciles"
(test-contact-verification-event
{:event [:activity-center.contact-verification/mark-as-trusted notification-id]
:expected-rpc-call {:method "wakuext_verifiedTrusted"
:params [{:id notification-id}]}}))
(testing "app db reconciliation"
(h/run-test-sync
(setup)
(h/stub-fx-with-callbacks :json-rpc/call
:on-success
(constantly contact-verification-rpc-response))
;; With "Unread" filter disabled
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/no-type :status :all}}])
(rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id])
(is (= [contact-verification-expected-notification]
(get-in (h/db) [:activity-center :notifications])))
;; With "Unread" filter enabled
(rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread])
(rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id])
(is (= [] (get-in (h/db) [:activity-center :notifications])))))
(testing "logs on failure"
(test-log-on-failure
{:notification-id notification-id
@ -316,11 +285,26 @@
:action :contact-verification/mark-as-trusted})))
(deftest contact-verification-mark-as-untrustworthy-test
(testing "marks notification as untrustworthy and reconciles"
(test-contact-verification-event
{:event [:activity-center.contact-verification/mark-as-untrustworthy notification-id]
:expected-rpc-call {:method "wakuext_verifiedUntrustworthy"
:params [{:id notification-id}]}}))
(testing "app db reconciliation"
(h/run-test-sync
(setup)
(h/stub-fx-with-callbacks
:json-rpc/call
:on-success
(constantly contact-verification-rpc-response))
;; With "Unread" filter disabled
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/no-type :status :all}}])
(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id])
(is (= [contact-verification-expected-notification]
(get-in (h/db) [:activity-center :notifications])))
;; With "Unread" filter enabled
(rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread])
(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id])
(is (= [] (get-in (h/db) [:activity-center :notifications])))))
(testing "logs on failure"
(test-log-on-failure
{:notification-id notification-id
@ -330,132 +314,100 @@
;;;; Notification reconciliation
(deftest notifications-reconcile-test
(testing "does nothing when there are no new notifications"
(h/run-test-sync
(setup)
(let [notifications {types/one-to-one-chat
{:all {:cursor ""
:data [{:id "0x1"
:read true
:type types/one-to-one-chat}
{:id "0x2"
:read false
:type types/one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x3"
:read false
:type types/one-to-one-chat}]}}
types/private-group-chat
{:unread {:cursor ""
:data [{:id "0x4"
:read false
:type types/private-group-chat}]}}}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
(rf/dispatch [:activity-center.notifications/reconcile nil])
(is (= notifications (get-in (h/db) [:activity-center :notifications]))))))
(testing "removes deleted notifications"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
notif-2 {:id "0x2" :read false :type types/one-to-one-chat}
notif-3 {:id "0x3" :read false :type types/system :dismissed true}
notif-4 {:id "0x4" :read true :type types/system}
notif-5 {:id "0x5" :read false :type types/system :accepted true}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:all {:cursor "" :data [notif-1 notif-2]}
:unread {:cursor "" :data [notif-2]}}
types/system
{:all {:cursor "" :data [notif-4]}
:unread {:cursor "" :data [notif-3 notif-5]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[(assoc notif-1 :deleted true)
(assoc notif-4 :deleted true)
notif-5]])
(is (= {types/no-type
{:all {:data [notif-5]}
:unread {:data [notif-5]}}
types/one-to-one-chat
{:all {:cursor "" :data [notif-2]}
:unread {:cursor "" :data [notif-2]}}
types/system
{:all {:cursor "" :data [notif-5]}
:unread {:cursor "" :data [notif-5 notif-3]}}}
(get-in (h/db) [:activity-center :notifications]))))))
(testing "replaces old notifications with newly arrived ones"
(testing "All tab + All filter"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
notif-4 {:id "0x4" :read false :type types/system}
notif-6 {:id "0x6" :read false :type types/system}
new-notif-1 (assoc notif-1 :last-message {})
new-notif-4 (assoc notif-4 :author "0xabc")]
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/no-type
{:all {:cursor "" :data [notif-1]}
:unread {:cursor "" :data [notif-4 notif-6]}}
types/one-to-one-chat
{:all {:cursor "" :data [notif-1]}}
types/system
{:unread {:cursor "" :data [notif-4 notif-6]}}}])
notif-2 {:id "0x2" :read false :type types/system}
new-notif-3 {:id "0x3" :read false :type types/system}
new-notif-4 {:id "0x4" :read true :type types/system}
new-notif-2 (assoc notif-2 :read true)]
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/no-type :status :all}
:notifications [notif-2 notif-1]}])
(rf/dispatch [:activity-center.notifications/reconcile [new-notif-1 new-notif-4 notif-6]])
(rf/dispatch
[:activity-center.notifications/reconcile
[(assoc notif-1 :deleted true) ; will be removed
new-notif-2
new-notif-3
new-notif-4]])
(is (= {types/no-type
{:all {:cursor "" :data [notif-6 new-notif-4 new-notif-1]}
:unread {:cursor "" :data [notif-6 new-notif-4]}}
types/one-to-one-chat
{:all {:cursor "" :data [new-notif-1]}
:unread {:data []}}
types/system
{:all {:data [notif-6 new-notif-4]}
:unread {:cursor "" :data [notif-6 new-notif-4]}}}
(is (= [new-notif-4 new-notif-3 new-notif-2]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "reconciles notifications that switched their read/unread status"
(testing "All tab + Unread filter"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
new-notif-1 (assoc notif-1 :read false)]
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:all {:cursor "" :data [notif-1]}}}])
(let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat}
notif-2 {:id "0x2" :read false :type types/system}
new-notif-2 (assoc notif-2 :read true)
new-notif-3 {:id "0x3" :read false :type types/system}
new-notif-4 {:id "0x4" :read true :type types/system}
notif-5 {:id "0x5" :type types/system}]
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/no-type :status :unread}
:notifications [notif-5 notif-2 notif-1]}])
(rf/dispatch [:activity-center.notifications/reconcile [new-notif-1]])
(rf/dispatch
[:activity-center.notifications/reconcile
[new-notif-2 ; will be removed because it's read
new-notif-3 ; will be inserted
new-notif-4 ; will be ignored because it's read
(assoc notif-5 :deleted true) ; will be removed
]])
(is (= {types/no-type
{:all {:data [new-notif-1]}
:unread {:data [new-notif-1]}}
types/one-to-one-chat
{:all {:cursor "" :data [new-notif-1]}
:unread {:data [new-notif-1]}}}
(is (= [new-notif-3 notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "membership notifications"
(testing "Contact request tab + All filter"
(h/run-test-sync
(setup)
(let [notif {:read false
:dismissed false
:accepted false
:type types/private-group-chat
:id "0x7"
:timestamp 1673445663000}]
(rf/dispatch [:activity-center.notifications/reconcile [notif]])
(let [notif-1 {:id "0x1" :read true :type types/contact-request}
notif-2 {:id "0x2" :read false :type types/contact-request}
new-notif-2 (assoc notif-2 :read true)
new-notif-3 {:id "0x3" :read false :type types/contact-request}
new-notif-4 {:id "0x4" :read true :type types/system}
notif-5 {:id "0x5" :read false :type types/contact-request}]
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/contact-request :status :all}
:notifications [notif-5 notif-2 notif-1]}])
(is (= {types/no-type
{:all {:data [notif]}
:unread {:data [notif]}}
(rf/dispatch
[:activity-center.notifications/reconcile
[new-notif-2 ; will be updated
new-notif-3 ; will be inserted
new-notif-4 ; will be ignored because it's not a contact request
(assoc notif-5 :deleted true) ; will be removed
]])
types/membership
{:all {:data [notif]}
:unread {:data [notif]}}}
(is (= [new-notif-3 new-notif-2 notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "Contact request tab + Unread filter"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :read false :type types/contact-request}
notif-2 {:id "0x2" :read false :type types/contact-request}
new-notif-2 (assoc notif-2 :read true)
new-notif-3 {:id "0x3" :read false :type types/contact-request}
new-notif-4 {:id "0x4" :read true :type types/contact-request}
new-notif-5 {:id "0x5" :read true :type types/system}
notif-6 {:id "0x6" :read false :type types/contact-request}]
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/contact-request :status :unread}
:notifications [notif-6 notif-2 notif-1]}])
(rf/dispatch
[:activity-center.notifications/reconcile
[new-notif-2 ; will be removed because it's read
new-notif-3 ; will be inserted
new-notif-4 ; will be ignored because it's read
new-notif-5 ; will be ignored because it's not a contact request
(assoc notif-6 :deleted true) ; will be removed
]])
(is (= [new-notif-3 notif-1]
(get-in (h/db) [:activity-center :notifications]))))))
;; Sorting by timestamp and ID is compatible with what the backend does when
@ -463,56 +415,38 @@
(testing "sorts notifications by timestamp and id in descending order"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat :timestamp 1}
notif-2 {:id "0x2" :read true :type types/one-to-one-chat :timestamp 1}
notif-3 {:id "0x3" :read false :type types/one-to-one-chat :timestamp 50}
notif-4 {:id "0x4" :read false :type types/one-to-one-chat :timestamp 100}
notif-5 {:id "0x5" :read false :type types/one-to-one-chat :timestamp 100}
(let [notif-1 {:id "0x1" :timestamp 1}
notif-2 {:id "0x2" :timestamp 1}
notif-3 {:id "0x3" :timestamp 50}
notif-4 {:id "0x4" :timestamp 100}
notif-5 {:id "0x5" :timestamp 100}
new-notif-1 (assoc notif-1 :last-message {})
new-notif-4 (assoc notif-4 :last-message {})]
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{types/one-to-one-chat
{:all {:cursor "" :data [notif-1 notif-2]}
:unread {:cursor "" :data [notif-3 notif-4 notif-5]}}}])
[notif-1 notif-3 notif-4 notif-2 notif-5]])
(rf/dispatch [:activity-center.notifications/reconcile [new-notif-1 new-notif-4]])
(is (= {types/no-type
{:all {:data [new-notif-4 new-notif-1]}
:unread {:data [new-notif-4]}}
types/one-to-one-chat
{:all {:cursor "" :data [new-notif-4 notif-2 new-notif-1]}
:unread {:cursor "" :data [notif-5 new-notif-4 notif-3]}}}
(is (= [notif-5 new-notif-4 notif-3 notif-2 new-notif-1]
(get-in (h/db) [:activity-center :notifications])))))))
(deftest remove-pending-contact-request-test
(testing "removes notification from all related filter types and status"
(h/run-test-sync
(setup)
(let [contact-pub-key "0x99"
notif-1 {:id "0x1" :read true :type types/contact-request}
notif-2 {:id "0x2" :read false :type types/contact-request :author contact-pub-key}
notif-3 {:id "0x3" :read false :type types/private-group-chat}
notifications {types/contact-request
{:all {:cursor "" :data [notif-2 notif-1]}
:unread {:cursor "" :data [notif-2]}}
types/private-group-chat
{:unread {:cursor "" :data [notif-3]}}
types/no-type
{:all {:cursor "" :data [notif-3 notif-2 notif-1]}
:unread {:cursor "" :data [notif-2 notif-3]}}}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
(let [author "0x99"
notif-1 {:id "0x1" :read true :type types/contact-request}
notif-2 {:id "0x2" :read false :type types/contact-request :author author}
notif-3 {:id "0x3" :read false :type types/private-group-chat :author author}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
[notif-3 ; will be ignored because it's not a contact request
notif-2 ; will be removed
notif-1 ; will be ignored because it's not from the same author
]])
(rf/dispatch [:activity-center/remove-pending-contact-request contact-pub-key])
(rf/dispatch [:activity-center/remove-pending-contact-request author])
(is (= {types/contact-request
{:all {:cursor "" :data [notif-1]}
:unread {:cursor "" :data []}}
types/private-group-chat
{:unread {:cursor "" :data [notif-3]}}
types/no-type
{:all {:cursor "" :data [notif-3 notif-1]}
:unread {:cursor "" :data [notif-3]}}}
(is (= [notif-3 notif-1]
(get-in (h/db) [:activity-center :notifications])))))))
;;;; Notifications fetching and pagination
@ -538,19 +472,18 @@
(is (= :unread (get-in (h/db) [:activity-center :filter :status])))
(is (= "" (get-in @spy-queue [0 :args 0 :params 0]))
"Should be called with empty cursor when fetching first page")
(is (= {types/one-to-one-chat
{:unread {:cursor "10"
:data [{:chat-id "0x9"
:chat-name nil
:chat-type types/one-to-one-chat
:group-chat false
:id "0x1"
:public? false
:last-message nil
:message nil
:read false
:reply-message nil
:type types/one-to-one-chat}]}}}
(is (= "10" (get-in (h/db) [:activity-center :cursor])))
(is (= [{:chat-id "0x9"
:chat-name nil
:chat-type types/one-to-one-chat
:group-chat false
:id "0x1"
:public? false
:last-message nil
:message nil
:read false
:reply-message nil
:type types/one-to-one-chat}]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "does not fetch next page when pagination cursor reached the end"
@ -558,36 +491,9 @@
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in
[:activity-center :notifications types/one-to-one-chat :unread :cursor]
""])
(rf/dispatch [:test/assoc-in [:activity-center :cursor] ""])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
;; The cursor can be nil sometimes because the reconciliation doesn't care
;; about updating the cursor value, but we have to make sure the next page is
;; only fetched if the current cursor is valid.
(testing "does not fetch next page when cursor is nil"
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in
[:activity-center :notifications types/one-to-one-chat :unread :cursor]
nil])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
(testing "fetches next page when pagination cursor is not empty"
@ -603,50 +509,27 @@
:read false
:chatId "0x9"}]}))
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/mention])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/mention :unread :cursor]
"10"])
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:status :unread :type types/mention}
:cursor "10"}])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= "wakuext_activityCenterNotificationsBy" (get-in @spy-queue [0 :args 0 :method])))
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))
"Should be called with current cursor")
(is (= {types/mention
{:unread {:cursor ""
:data [{:chat-id "0x9"
:chat-name nil
:chat-type 3
:id "0x1"
:last-message nil
:message nil
:read false
:reply-message nil
:type types/mention}]}}}
(is (= "" (get-in (h/db) [:activity-center :cursor])))
(is (= [{:chat-id "0x9"
:chat-name nil
:chat-type 3
:id "0x1"
:last-message nil
:message nil
:read false
:reply-message nil
:type types/mention}]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "does not fetch next page while it is still loading"
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:all])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications types/one-to-one-chat :all :cursor]
"10"])
(rf/dispatch [:test/assoc-in
[:activity-center :notifications types/one-to-one-chat :all :loading?]
true])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= [] @spy-queue)))))
(testing "resets loading flag after an error"
(h/run-test-sync
(setup)
@ -654,41 +537,19 @@
(h/stub-fx-with-callbacks :json-rpc/call :on-error (constantly :fake-error))
(h/spy-event-fx spy-queue :activity-center.notifications/fetch-error)
(h/spy-fx spy-queue :json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
types/one-to-one-chat])
(rf/dispatch [:test/assoc-in
[:activity-center :notifications types/one-to-one-chat :unread :cursor]
""])
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:status :unread :type types/one-to-one-chat}
:cursor ""}])
(rf/dispatch [:activity-center.notifications/fetch-first-page])
(is (nil? (get-in (h/db)
[:activity-center :notifications types/one-to-one-chat :unread :loading?])))
(is (nil? (get-in (h/db) [:activity-center :loading?])))
(is (= [:activity-center.notifications/fetch-error
types/one-to-one-chat
:unread
:fake-error]
(:args (last @spy-queue))))))))
(deftest notifications-fetch-unread-contact-requests-test
(testing "fetches latest unread contact requests"
(let [actual (activity-center/notifications-fetch-unread-contact-requests {:db {}})
per-page 20]
(is (= {:activity-center
{:notifications
{types/contact-request
{:unread {:loading? true}}}}}
(:db actual)))
(is (= {:method "wakuext_activityCenterNotificationsBy"
:params ["" per-page [types/contact-request] activity-center/status-unread true]}
(-> actual
:json-rpc/call
first
(select-keys [:method :params])))))))
(deftest notifications-fetch-unread-count-test
(testing "fetches total notification count and store in db"
(h/run-test-sync

View File

@ -134,7 +134,7 @@
[gesture/touchable-without-feedback
{:on-press (fn []
(rf/dispatch [:hide-popover])
(rf/dispatch [:chat.ui/start-chat {:public-key author}]))}
(rf/dispatch [:chat.ui/start-chat author]))}
[incoming-contact-request-view notification set-swipeable-height]]
:else

View File

@ -209,7 +209,7 @@
(rn/use-effect-once #(rf/dispatch [:activity-center.notifications/fetch-first-page]))
[safe-area/consumer
(fn [{:keys [top bottom]}]
(let [notifications (rf/sub [:activity-center/filtered-notifications])
(let [notifications (rf/sub [:activity-center/notifications])
window-width (rf/sub [:dimensions/window-width])]
[rn/view {:style (style/screen-container window-width top bottom)}
[header request-close]

View File

@ -51,7 +51,8 @@
chats-js (.-chatsForContacts response-js)
events (reduce
(prepare-events-for-contact db chats-js)
[[:activity-center.notifications/fetch-unread-count]]
[[:activity-center.notifications/fetch-unread-count]
[:activity-center.notifications/fetch-pending-contact-requests]]
contacts-cljs)]
(js-delete response-js "contacts")
(js-delete response-js "chatsForContacts")

View File

@ -1,6 +1,5 @@
(ns status-im2.subs.activity-center
(:require [re-frame.core :as re-frame]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification-types :as types]))
(re-frame/reg-sub
@ -53,14 +52,6 @@
(fn [activity-center]
(get-in activity-center [:filter :type] types/no-type)))
(re-frame/reg-sub
:activity-center/filtered-notifications
:<- [:activity-center/filter-type]
:<- [:activity-center/filter-status]
:<- [:activity-center/notifications]
(fn [[filter-type filter-status notifications]]
(get-in notifications [filter-type filter-status :data])))
(re-frame/reg-sub
:activity-center/filter-status-unread-enabled?
:<- [:activity-center/filter-status]
@ -69,9 +60,6 @@
(re-frame/reg-sub
:activity-center/pending-contact-requests
:<- [:activity-center/notifications]
(fn [notifications]
(filter (fn [{:keys [message]}]
(= constants/contact-request-message-state-pending
(:contact-request-state message)))
(get-in notifications [types/contact-request :unread :data]))))
:<- [:activity-center]
(fn [activity-center]
(:contact-requests activity-center)))

View File

@ -1,7 +1,6 @@
(ns status-im2.subs.activity-center-test
(:require [cljs.test :refer [is testing]]
[re-frame.db :as rf-db]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification-types :as types]
status-im2.subs.activity-center
[test-helpers.unit :as h]
@ -64,27 +63,3 @@
types/admin 7})
(is (= 28 (rf/sub [sub-name]))))
(h/deftest-sub :activity-center/pending-contact-requests
[sub-name]
(testing "returns only contact request notifications in the pending state"
(let [pending {:id "0x2"
:type types/contact-request
:message {:contact-request-state
constants/contact-request-message-state-pending}}]
(swap! rf-db/app-db assoc-in
[:activity-center :notifications types/contact-request :unread :data]
[{:id "0x1"
:type types/contact-request
:message {:contact-request-state constants/contact-request-message-state-none}}
pending
{:id "0x3"
:type types/contact-request
:message {:contact-request-state constants/contact-request-message-state-accepted}}
{:id "0x4"
:type types/contact-request
:message {:contact-request-state constants/contact-request-message-state-declined}}
{:id "0x5"
:type types/mention}])
(is (= [pending] (rf/sub [sub-name]))))))