[#13968] Fetch and reconcile notifications by type and read status (#14125)

- Extract tabs component from the main screen component to reduce rendering cost when switching tabs.
- Display empty state component when there are no notifications.
- Fix screen flickering due to quickly flushing and filling the db state.
- Display top bar as a sticky header.
- Namespace icon keywords.
- Correctly sorts notifications during reconciliation.
- Remove warning about children without unique key.
This commit is contained in:
Icaro Motta 2022-10-10 16:33:42 -03:00 committed by GitHub
parent adfb0ddb5d
commit af5f979443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 509 additions and 215 deletions

View File

@ -4,6 +4,7 @@
[quo2.components.tabs.tab :as tab]
[reagent.core :as reagent]
[status-im.ui.components.react :as react]
[status-im.utils.core :as utils]
[status-im.utils.number :as number-utils]))
(def default-tab-size 32)
@ -82,8 +83,8 @@
(let [maybe-mask-wrapper (if fade-end?
[react/masked-view
{:mask-element (reagent/as-element
[react/linear-gradient {:colors ["black" "transparent"]
:locations [(@fading :fade-end-percentage) 1]
[react/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
@ -99,7 +100,9 @@
:on-change
:scroll-on-press?
:size)
{:ref (partial reset! flat-list-ref)
(when scroll-on-press?
{:initial-scroll-index (utils/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
@ -116,7 +119,7 @@
:layout-width layout-width
:max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering.
(when (not= new-percentage (@fading :fade-end-percentage))
(when (not= new-percentage (get @fading :fade-end-percentage))
(swap! fading assoc :fade-end-percentage new-percentage))))
(when on-scroll
(on-scroll e)))

View File

@ -1,5 +1,6 @@
(ns status-im.activity-center.core
(:require [re-frame.core :as re-frame]
[status-im.constants :as constants]
[status-im.data-store.activities :as data-store.activities]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.fx :as fx]
@ -8,90 +9,117 @@
;;;; Notification reconciliation
(defn- update-notifications
[old new]
(let [ids-to-be-removed (->> new
(filter #(or (:dismissed %) (:accepted %)))
(map :id))
grouped-new (apply dissoc (group-by :id new) ids-to-be-removed)
grouped-old (apply dissoc (group-by :id old) ids-to-be-removed)]
(->> (merge grouped-old grouped-new)
vals
(map first))))
"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 type read] :as notification}]
(let [filter-status (if read :read :unread)]
(cond-> (-> acc
(update-in [type :read :data]
(fn [data]
(remove #(= id (:id %)) data)))
(update-in [type :unread :data]
(fn [data]
(remove #(= id (:id %)) data))))
(not (or (:dismissed notification) (:accepted notification)))
(update-in [type filter-status :data]
(fn [data]
(->> notification
(conj data)
(sort-by (juxt :timestamp :id))
reverse))))))
db-notifications
new-notifications))
(fx/defn notifications-reconcile
{:events [:activity-center.notifications/reconcile]}
[{:keys [db]} new-notifications]
(let [{read-new true
unread-new false} (group-by :read new-notifications)
read-old (get-in db [:activity-center :notifications-read :data])
unread-old (get-in db [:activity-center :notifications-unread :data])]
{:db (-> db
(assoc-in [:activity-center :notifications-read :data]
(update-notifications read-old read-new))
(assoc-in [:activity-center :notifications-unread :data]
(update-notifications unread-old unread-new)))}))
(when (seq new-notifications)
{:db (update-in db [:activity-center :notifications]
update-notifications new-notifications)}))
;;;; Notifications fetching and pagination
(def notifications-per-page
20)
(def defaults
{:filter-status :unread
:filter-type constants/activity-center-notification-type-no-type
:notifications-per-page 10})
(def start-or-end-cursor
"")
(defn notifications-group->rpc-method
[notifications-group]
(if (= notifications-group :notifications-read)
(defn- valid-cursor?
[cursor]
(and (some? cursor)
(not= cursor start-or-end-cursor)))
(defn- filter-status->rpc-method
[filter-status]
(if (= filter-status :read)
"wakuext_readActivityCenterNotifications"
"wakuext_unreadActivityCenterNotifications"))
(defn notifications-read-status->group
[status-filter]
(if (= status-filter :read)
:notifications-read
:notifications-unread))
(fx/defn notifications-fetch
[{:keys [db]} cursor notifications-group]
(when-not (get-in db [:activity-center notifications-group :loading?])
{:db (assoc-in db [:activity-center notifications-group :loading?] true)
::json-rpc/call [{:method (notifications-group->rpc-method notifications-group)
:params [cursor notifications-per-page]
:on-success #(re-frame/dispatch [:activity-center.notifications/fetch-success notifications-group %])
:on-error #(re-frame/dispatch [:activity-center.notifications/fetch-error notifications-group %])}]}))
[{:keys [db]} {:keys [cursor filter-type filter-status reset-data?]}]
(when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?])
{:db (assoc-in db [:activity-center :notifications filter-type filter-status :loading?] true)
::json-rpc/call [{:method (filter-status->rpc-method filter-status)
:params [cursor (defaults :notifications-per-page) filter-type]
:on-success #(re-frame/dispatch [:activity-center.notifications/fetch-success filter-type filter-status reset-data? %])
:on-error #(re-frame/dispatch [:activity-center.notifications/fetch-error filter-type filter-status %])}]}))
(fx/defn notifications-fetch-first-page
{:events [:activity-center.notifications/fetch-first-page]}
[{:keys [db] :as cofx} {:keys [status-filter] :or {status-filter :unread}}]
(let [notifications-group (notifications-read-status->group status-filter)]
[{:keys [db] :as cofx} {:keys [filter-type filter-status]}]
(let [filter-type (or filter-type
(get-in db [:activity-center :filter :type]
(defaults :filter-type)))
filter-status (or filter-status
(get-in db [:activity-center :filter :status]
(defaults :filter-status)))]
(fx/merge cofx
{:db (-> db
(assoc-in [:activity-center :current-status-filter] status-filter)
(update-in [:activity-center notifications-group] dissoc :loading?)
(update-in [:activity-center notifications-group] dissoc :data))}
(notifications-fetch start-or-end-cursor notifications-group))))
(assoc-in [:activity-center :filter :type] filter-type)
(assoc-in [:activity-center :filter :status] filter-status))}
(notifications-fetch {:cursor start-or-end-cursor
:filter-type filter-type
:filter-status filter-status
:reset-data? true}))))
(fx/defn notifications-fetch-next-page
{:events [:activity-center.notifications/fetch-next-page]}
[{:keys [db] :as cofx}]
(let [status-filter (get-in db [:activity-center :current-status-filter])
notifications-group (notifications-read-status->group status-filter)
{:keys [cursor]} (get-in db [:activity-center notifications-group])]
(when-not (= cursor start-or-end-cursor)
(notifications-fetch cofx cursor notifications-group))))
(let [{:keys [type status]} (get-in db [:activity-center :filter])
{:keys [cursor]} (get-in db [:activity-center :notifications type status])]
(when (valid-cursor? cursor)
(notifications-fetch cofx {:cursor cursor
:filter-type type
:filter-status status
:reset-data? false}))))
(fx/defn notifications-fetch-success
{:events [:activity-center.notifications/fetch-success]}
[{:keys [db]} notifications-group {:keys [cursor notifications]}]
{:db (-> db
(update-in [:activity-center notifications-group] dissoc :loading?)
(assoc-in [:activity-center notifications-group :cursor] cursor)
(update-in [:activity-center notifications-group :data]
concat
(map data-store.activities/<-rpc notifications)))})
[{:keys [db]}
filter-type
filter-status
reset-data?
{:keys [cursor notifications]}]
(let [processed (map data-store.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]
(if reset-data?
(constantly processed)
#(concat %1 processed))))}))
(fx/defn notifications-fetch-error
{:events [:activity-center.notifications/fetch-error]}
[{:keys [db]} notifications-group error]
[{:keys [db]} filter-type filter-status error]
(log/warn "Failed to load Activity Center notifications" error)
{:db (update-in db [:activity-center notifications-group] dissoc :loading?)})
{:db (update-in db [:activity-center :notifications filter-type filter-status] dissoc :loading?)})

View File

@ -2,99 +2,285 @@
(:require [cljs.test :refer [deftest is testing]]
[day8.re-frame.test :as rf-test]
[re-frame.core :as rf]
[status-im.constants :as c]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.test-helpers :as h]
status-im.events))
status-im.events
[status-im.test-helpers :as h]))
(defn setup []
(h/register-helper-events)
(rf/dispatch [:init/app-started]))
(defn remove-color-key
"Remove `:color` key from notifications because they have random values that we
can't assert against."
[grouped-notifications {:keys [type status]}]
(update-in grouped-notifications
[type status :data]
(fn [old _]
(map #(dissoc % :color) old))
nil))
(deftest notifications-reconcile-test
(testing "does nothing when there are no new notifications"
(rf-test/run-test-sync
(setup)
(let [read [{:id "0x1" :read true}]
unread [{:id "0x4" :read false}]]
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data] read])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data] unread])
(let [notifications {c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat}
{:id "0x2"
:read true
:type c/activity-center-notification-type-one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x3"
:read false
:type c/activity-center-notification-type-one-to-one-chat}]}}
c/activity-center-notification-type-private-group-chat
{:unread {:cursor ""
:data [{:id "0x4"
:read false
:type c/activity-center-notification-type-private-group-chat}]}}}]
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
(rf/dispatch [:activity-center.notifications/reconcile nil])
(is (= read (get-in (h/db) [:activity-center :notifications-read :data])))
(is (= unread (get-in (h/db) [:activity-center :notifications-unread :data]))))))
(is (= notifications (get-in (h/db) [:activity-center :notifications]))))))
(testing "removes dismissed or accepted notifications"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data]
[{:id "0x1" :read true}
{:id "0x2" :read true}
{:id "0x3" :read true}]])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data]
[{:id "0x4" :read false}
{:id "0x5" :read false}
{:id "0x6" :read false}]])
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat}
{:id "0x2" :read true :type c/activity-center-notification-type-one-to-one-chat}]}
:unread {:cursor ""
:data [{:id "0x3" :read false :type c/activity-center-notification-type-one-to-one-chat}]}}
2 {:unread {:cursor ""
:data [{:id "0x4" :read false :type 2}
{:id "0x6" :read false :type 2}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :dismissed true}
{:id "0x3" :read true :accepted true}
{:id "0x4" :read false :dismissed true}
{:id "0x5" :read false :accepted true}]])
[{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat
:dismissed true}
{:id "0x3"
:read false
:type c/activity-center-notification-type-one-to-one-chat
:accepted true}
{:id "0x4"
:read false
:type c/activity-center-notification-type-private-group-chat
:dismissed true}
{:id "0x5"
:read false
:type c/activity-center-notification-type-private-group-chat
:accepted true}]])
(is (= [{:id "0x2" :read true}]
(get-in (h/db) [:activity-center :notifications-read :data])))
(is (= [{:id "0x6" :read false}]
(get-in (h/db) [:activity-center :notifications-unread :data])))))
(is (= {c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x2"
:read true
:type c/activity-center-notification-type-one-to-one-chat}]}
:unread {:cursor ""
:data []}}
c/activity-center-notification-type-private-group-chat
{:read {:data []}
:unread {:cursor ""
:data [{:id "0x6"
:read false
:type c/activity-center-notification-type-private-group-chat}]}}}
(get-in (h/db) [:activity-center :notifications])))))
(testing "replaces old notifications with newly arrived ones"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :data]
[{:id "0x1" :read true}
{:id "0x2" :read true}]])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :data]
[{:id "0x3" :read false}
{:id "0x4" :read false}]])
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat}]}}
c/activity-center-notification-type-private-group-chat
{:unread {:cursor ""
:data [{:id "0x4"
:read false
:type c/activity-center-notification-type-private-group-chat}
{:id "0x6"
:read false
:type c/activity-center-notification-type-private-group-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :name "ABC"}
{:id "0x3" :read false :name "XYZ"}]])
[{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat
:last-message {}}
{:id "0x4"
:read false
:type c/activity-center-notification-type-private-group-chat
:author "0xabc"}
{:id "0x6"
:read false
:type c/activity-center-notification-type-private-group-chat}]])
(is (= [{:id "0x1" :read true :name "ABC"}
{:id "0x2" :read true}]
(get-in (h/db) [:activity-center :notifications-read :data])))
(is (= [{:id "0x3" :read false :name "XYZ"}
{:id "0x4" :read false}]
(get-in (h/db) [:activity-center :notifications-unread :data]))))))
(is (= {c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat
:last-message {}}]}
:unread {:data []}}
c/activity-center-notification-type-private-group-chat
{:read {:data []}
:unread {:cursor ""
:data [{:id "0x6"
:read false
:type c/activity-center-notification-type-private-group-chat}
{:id "0x4"
:read false
:type c/activity-center-notification-type-private-group-chat
:author "0xabc"}]}}}
(get-in (h/db) [:activity-center :notifications])))))
(testing "reconciles notifications that switched their read/unread status"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1"
:read false
:type c/activity-center-notification-type-one-to-one-chat}]])
(is (= {c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data []}
:unread {:data [{:id "0x1"
:read false
:type c/activity-center-notification-type-one-to-one-chat}]}}}
(get-in (h/db) [:activity-center :notifications])))))
;; Sorting by timestamp and ID is compatible with what the backend does when
;; returning paginated results.
(testing "sorts notifications by timestamp and id in descending order"
(rf-test/run-test-sync
(setup)
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
{c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1}
{:id "0x2" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1}]}
:unread {:cursor ""
:data [{:id "0x3" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 50}
{:id "0x4" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100}
{:id "0x5" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100}]}}}])
(rf/dispatch [:activity-center.notifications/reconcile
[{:id "0x1" :read true :type c/activity-center-notification-type-one-to-one-chat :timestamp 1 :last-message {}}
{:id "0x4" :read false :type c/activity-center-notification-type-one-to-one-chat :timestamp 100 :last-message {}}]])
(is (= {c/activity-center-notification-type-one-to-one-chat
{:read {:cursor ""
:data [{:id "0x2"
:read true
:type c/activity-center-notification-type-one-to-one-chat
:timestamp 1}
{:id "0x1"
:read true
:type c/activity-center-notification-type-one-to-one-chat
:timestamp 1
:last-message {}}]}
:unread {:cursor ""
:data [{:id "0x5"
:read false
:type c/activity-center-notification-type-one-to-one-chat
:timestamp 100}
{:id "0x4"
:read false
:type c/activity-center-notification-type-one-to-one-chat
:timestamp 100
:last-message {}}
{:id "0x3"
:read false
:type c/activity-center-notification-type-one-to-one-chat
:timestamp 50}]}}}
(get-in (h/db) [:activity-center :notifications]))))))
(deftest notifications-fetch-test
(testing "fetches first page"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks ::json-rpc/call
:on-success (constantly {:cursor "10"
:notifications [{:chatId "0x1"}]}))
(h/stub-fx-with-callbacks
::json-rpc/call
:on-success (constantly {:cursor "10"
:notifications [{:id "0x1"
:type c/activity-center-notification-type-one-to-one-chat
:read false
:chatId "0x9"}]}))
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:activity-center.notifications/fetch-first-page])
(rf/dispatch [:activity-center.notifications/fetch-first-page
{:filter-type c/activity-center-notification-type-one-to-one-chat}])
(is (= :unread (get-in (h/db) [:activity-center :current-status-filter])))
(is (nil? (get-in (h/db) [:activity-center :notifications-unread :loading?])))
(is (= "10" (get-in (h/db) [:activity-center :notifications-unread :cursor])))
(is (= "" (get-in @spy-queue [0 :args 0 :params 0])))
(is (= [{:chat-id "0x1"}]
(->> (get-in (h/db) [:activity-center :notifications-unread :data])
(map #(select-keys % [:chat-id]))))))))
(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 (= {c/activity-center-notification-type-one-to-one-chat
{:unread {:cursor "10"
:data [{:chat-id "0x9"
:chat-name nil
:chat-type c/activity-center-notification-type-one-to-one-chat
:group-chat false
:id "0x1"
:public? false
:last-message nil
:message nil
:read false
:reply-message nil
:type c/activity-center-notification-type-one-to-one-chat}]}}}
(remove-color-key (get-in (h/db) [:activity-center :notifications])
{:status :unread
:type c/activity-center-notification-type-one-to-one-chat}))))))
(testing "does not fetch next page when pagination cursor reached the end"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :unread])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""])
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
c/activity-center-notification-type-one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :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"
(rf-test/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]
c/activity-center-notification-type-one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor]
nil])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
@ -104,30 +290,54 @@
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/stub-fx-with-callbacks ::json-rpc/call
:on-success (constantly {:cursor ""
:notifications [{:chatId "0x1"}]}))
(h/stub-fx-with-callbacks
::json-rpc/call
:on-success (constantly {:cursor ""
:notifications [{:id "0x1"
:type c/activity-center-notification-type-mention
:read false
:chatId "0x9"}]}))
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :unread])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] "10"])
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
c/activity-center-notification-type-mention])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-mention :unread :cursor]
"10"])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
(is (= "wakuext_unreadActivityCenterNotifications" (get-in @spy-queue [0 :args 0 :method])))
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0])))
(is (= "" (get-in (h/db) [:activity-center :notifications-unread :cursor])))
(is (= [{:chat-id "0x1"}]
(->> (get-in (h/db) [:activity-center :notifications-unread :data])
(map #(select-keys % [:chat-id]))))))))
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0]))
"Should be called with current cursor")
(is (= {c/activity-center-notification-type-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 c/activity-center-notification-type-mention}]}}}
(remove-color-key (get-in (h/db) [:activity-center :notifications])
{:status :unread
:type c/activity-center-notification-type-mention}))))))
(testing "does not fetch next page while it is still loading"
(rf-test/run-test-sync
(setup)
(let [spy-queue (atom [])]
(h/spy-fx spy-queue ::json-rpc/call)
(rf/dispatch [:test/assoc-in [:activity-center :current-status-filter] :read])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :cursor] "10"])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-read :loading?] true])
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:read])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
c/activity-center-notification-type-one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :read :cursor]
"10"])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :read :loading?]
true])
(rf/dispatch [:activity-center.notifications/fetch-next-page])
@ -140,13 +350,18 @@
(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 :current-status-filter] :unread])
(rf/dispatch [:test/assoc-in [:activity-center :notifications-unread :cursor] ""])
(rf/dispatch [:test/assoc-in [:activity-center :filter :status]
:unread])
(rf/dispatch [:test/assoc-in [:activity-center :filter :type]
c/activity-center-notification-type-one-to-one-chat])
(rf/dispatch [:test/assoc-in [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :cursor]
""])
(rf/dispatch [:activity-center.notifications/fetch-first-page])
(is (nil? (get-in (h/db) [:activity-center :notifications-unread :loading?])))
(is (nil? (get-in (h/db) [:activity-center :notifications c/activity-center-notification-type-one-to-one-chat :unread :loading?])))
(is (= [:activity-center.notifications/fetch-error
:notifications-unread
c/activity-center-notification-type-one-to-one-chat
:unread
:fake-error]
(:args (last @spy-queue))))))))

View File

@ -176,6 +176,7 @@
(def ^:const docs-link "https://status.im/docs/")
(def ^:const principles-link "https://our.status.im/our-principles/")
(def ^:const activity-center-notification-type-no-type 0)
(def ^:const activity-center-notification-type-one-to-one-chat 1)
(def ^:const activity-center-notification-type-private-group-chat 2)
(def ^:const activity-center-notification-type-mention 3)
@ -183,6 +184,13 @@
(def ^:const activity-center-notification-type-contact-request 5)
(def ^:const activity-center-notification-type-contact-request-retracted 6)
;; TODO: Replace with correct enum values once status-go implements them.
(def ^:const activity-center-notification-type-admin 66610)
(def ^:const activity-center-notification-type-identity-verification 66611)
(def ^:const activity-center-notification-type-tx 66612)
(def ^:const activity-center-notification-type-membership 66613)
(def ^:const activity-center-notification-type-system 66614)
(def ^:const visibility-status-unknown 0)
(def ^:const visibility-status-automatic 1)
(def ^:const visibility-status-dnd 2)

View File

@ -6,35 +6,36 @@
[clojure.string :as string]))
(re-frame/reg-sub
:activity-center/notifications-read
(fn [db]
(get-in db [:activity-center :notifications-read :data])))
:activity-center/notifications
:<- [:activity-center]
(fn [activity-center]
(:notifications activity-center)))
(re-frame/reg-sub
:activity-center/notifications-unread
(fn [db]
(get-in db [:activity-center :notifications-unread :data])))
:activity-center/filter-status
:<- [:activity-center]
(fn [activity-center]
(get-in activity-center [:filter :status])))
(re-frame/reg-sub
:activity-center/current-status-filter
(fn [db]
(get-in db [:activity-center :current-status-filter])))
:activity-center/filter-type
:<- [:activity-center]
(fn [activity-center]
(get-in activity-center [:filter :type] constants/activity-center-notification-type-no-type)))
(re-frame/reg-sub
:activity-center/status-filter-unread-enabled?
:<- [:activity-center/current-status-filter]
(fn [current-status-filter]
(= :unread current-status-filter)))
: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/notifications-per-read-status
:<- [:activity-center/notifications-read]
:<- [:activity-center/notifications-unread]
:<- [:activity-center/status-filter-unread-enabled?]
(fn [[notifications-read notifications-unread unread-filter-enabled?]]
(if unread-filter-enabled?
notifications-unread
notifications-read)))
:activity-center/filter-status-unread-enabled?
:<- [:activity-center/filter-status]
(fn [filter-status]
(= :unread filter-status)))
(defn- group-notifications-by-date
[notifications]

View File

@ -228,6 +228,7 @@
(reg-root-key-sub :activity.center/notifications :activity.center/notifications)
(reg-root-key-sub :activity.center/notifications-count :activity.center/notifications-count)
(reg-root-key-sub :activity-center :activity-center)
(reg-root-key-sub :bug-report/description-error :bug-report/description-error)
(reg-root-key-sub :bug-report/details :bug-report/details)

View File

@ -1,5 +1,6 @@
(ns status-im.ui.screens.activity-center.views
(:require [quo.components.animated.pressable :as animation]
[quo.design-system.colors :as quo.colors]
[quo.react-native :as rn]
[quo2.components.buttons.button :as button]
[quo2.components.markdown.text :as text]
@ -14,9 +15,6 @@
[status-im.utils.datetime :as datetime]
[status-im.utils.handlers :refer [<sub >evt]]))
(defonce selected-activity-type
(reagent/atom :activity-type/all))
(defn activity-title
[{:keys [type]}]
(case type
@ -32,8 +30,8 @@
[{:keys [type]}]
(case type
constants/activity-center-notification-type-contact-request
:add-user
:placeholder))
:main-icons2/add-user
:main-icons2/placeholder))
(defn activity-context
[{:keys [message last-message type]}]
@ -81,7 +79,7 @@
nil))
(defn activity-pressable
[notification & children]
[notification activity]
(case (get-in notification [:message :contact-request-state])
constants/contact-request-message-state-accepted
;; NOTE [2022-09-21]: We need to dispatch to
@ -89,14 +87,13 @@
;; `:chat.ui/navigate-to-chat`, otherwise the chat screen looks completely
;; broken if it has never been opened before for the accepted contact.
[animation/pressable {:on-press #(>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}])}
children]
[:<> children]))
activity]
activity))
(defn render-notification
[notification index]
[rn/view {:flex 1
:flex-direction :column
:margin-top (if (= 0 index) 0 4)}
[rn/view {:margin-top (if (= 0 index) 0 4)
:padding-horizontal 20}
[activity-pressable notification
[activity-logs/activity-log
(merge {:context (activity-context notification)
@ -108,67 +105,108 @@
:unread? (not (:read notification))}
(activity-buttons notification))]]])
(defn notifications-list
(defn filter-selector-read-toggle
[]
(let [notifications (<sub [:activity-center/notifications-per-read-status])]
[rn/flat-list {:data notifications
:key-fn :id
:on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
:render-fn render-notification}]))
(defn filter-selector-read []
(let [unread-filter-enabled? (<sub [:activity-center/status-filter-unread-enabled?])]
(let [unread-filter-enabled? (<sub [:activity-center/filter-status-unread-enabled?])]
;; TODO: Replace the button by a Filter Selector component once available for use.
[button/button {:icon true
:type (if unread-filter-enabled? :primary :outline)
:size 32
:on-press #(if unread-filter-enabled?
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}])
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]))}
:unread]))
:on-press #(>evt [:activity-center.notifications/fetch-first-page
{:filter-status (if unread-filter-enabled?
:read
:unread)}])}
:main-icons2/unread]))
(defn activity-center []
;; TODO(2022-10-07): The empty state is still under design analysis, so we
;; shouldn't even care about translations at this point. A placeholder box is
;; used instead of an image.
(defn empty-tab
[]
[rn/view {:style {:align-items :center
:flex 1
:justify-content :center
:padding-vertical 12}}
[rn/view {:style {:background-color colors/neutral-80
:height 120
:margin-bottom 20
:width 120}}]
[text/text {:size :paragraph-1
:style {:padding-bottom 2}
:weight :semi-bold}
"No notifications"]
[text/text {:size :paragraph-2}
"Your notifications will be here"]])
(defn tabs
[]
(let [filter-type (<sub [:activity-center/filter-type])]
[tabs/scrollable-tabs {:size 32
:style {:padding-left 20}
:fade-end-percentage 0.79
:scroll-on-press? true
:fade-end? true
:on-change #(>evt [:activity-center.notifications/fetch-first-page {:filter-type %}])
:default-active filter-type
:data [{:id constants/activity-center-notification-type-no-type
:label (i18n/label :t/all)}
{:id constants/activity-center-notification-type-admin
:label (i18n/label :t/admin)}
{:id constants/activity-center-notification-type-mention
:label (i18n/label :t/mentions)}
{:id constants/activity-center-notification-type-reply
:label (i18n/label :t/replies)}
{:id constants/activity-center-notification-type-contact-request
:label (i18n/label :t/contact-requests)}
{:id constants/activity-center-notification-type-identity-verification
:label (i18n/label :t/identity-verification)}
{:id constants/activity-center-notification-type-tx
:label (i18n/label :t/transactions)}
{:id constants/activity-center-notification-type-membership
:label (i18n/label :t/membership)}
{:id constants/activity-center-notification-type-system
:label (i18n/label :t/system)}]}]))
(defn header
[]
(let [screen-padding 20]
;; TODO: Remove temporary (and old) background color when the screen and
;; header are properly blurred.
[rn/view {:background-color (:ui-background @quo.colors/theme)}
[button/button {:icon true
:type :grey
:size 32
:style {:margin-vertical 12
:margin-left screen-padding}
:on-press #(>evt [:navigate-back])}
:main-icons2/close]
[text/text {:size :heading-1
:weight :semi-bold
:style {:padding-horizontal screen-padding
:padding-vertical 12}}
(i18n/label :t/notifications)]
[rn/view {:flex-direction :row
:padding-vertical 12}
[rn/view {:flex 1
:align-self :stretch}
[tabs]]
[rn/view {:flex-grow 0
:margin-left 16
:padding-right screen-padding}
[filter-selector-read-toggle]]]]))
(defn activity-center
[]
(reagent/create-class
{:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}])
{:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page])
:reagent-render
(fn []
(let [screen-padding 20]
[:<>
[button/button {:icon true
:type :grey
:size 32
:style {:margin-vertical 12
:margin-left screen-padding}
:on-press #(>evt [:navigate-back])}
:close]
[text/text {:size :heading-1
:weight :semi-bold
:style {:padding-horizontal screen-padding
:padding-vertical 12}}
(i18n/label :t/notifications)]
[rn/view {:flex-direction :row
:padding-vertical 12}
[rn/view {:flex 1
:align-self :stretch}
[tabs/scrollable-tabs {:size 32
:style {:padding-left screen-padding}
:fade-end-percentage 0.79
:scroll-on-press? true
:fade-end? true
:on-change (partial reset! selected-activity-type)
:default-active :activity-type/all
:data [{:id :activity-type/all :label (i18n/label :t/all)}
{:id :activity-type/admin :label (i18n/label :t/admin)}
{:id :activity-type/mention :label (i18n/label :t/mentions)}
{:id :activity-type/reply :label (i18n/label :t/replies)}
{:id :activity-type/contact-request :label (i18n/label :t/contact-requests)}
{:id :activity-type/identity-verification :label (i18n/label :t/identity-verification)}
{:id :activity-type/transaction :label (i18n/label :t/transactions)}
{:id :activity-type/membership :label (i18n/label :t/membership)}
{:id :activity-type/system :label (i18n/label :t/system)}]}]]
[rn/view {:flex-grow 0
:margin-left 16
:padding-right screen-padding}
[filter-selector-read]]]
[rn/view {:padding-horizontal screen-padding}
[notifications-list]]]))}))
(let [notifications (<sub [:activity-center/filtered-notifications])]
[rn/flat-list {:content-container-style {:flex-grow 1}
:data notifications
:empty-component [empty-tab]
:header [header]
:key-fn :id
:on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
:render-fn render-notification
:sticky-header-indices [0]}]))}))