- 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:
parent
adfb0ddb5d
commit
af5f979443
|
@ -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)))
|
||||
|
|
|
@ -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?)})
|
||||
|
|
|
@ -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))))))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]}]))}))
|
||||
|
|
Loading…
Reference in New Issue