diff --git a/resources/images/icons/chevron_down@2x.png b/resources/images/icons/chevron_down@2x.png new file mode 100644 index 0000000000..0fc2c2eddc Binary files /dev/null and b/resources/images/icons/chevron_down@2x.png differ diff --git a/resources/images/icons/chevron_down@3x.png b/resources/images/icons/chevron_down@3x.png new file mode 100644 index 0000000000..edc9820def Binary files /dev/null and b/resources/images/icons/chevron_down@3x.png differ diff --git a/resources/images/icons/tiny_community@2x.png b/resources/images/icons/tiny_community@2x.png new file mode 100644 index 0000000000..ff26bbb846 Binary files /dev/null and b/resources/images/icons/tiny_community@2x.png differ diff --git a/resources/images/icons/tiny_community@3x.png b/resources/images/icons/tiny_community@3x.png new file mode 100644 index 0000000000..97f43b69fd Binary files /dev/null and b/resources/images/icons/tiny_community@3x.png differ diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 14679cbd98..f6b4f25b91 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -151,3 +151,7 @@ (def ^:const privacy-policy-link "https://status.im/privacy-policy/") (def ^:const terms-of-service-link "https://status.im/terms-of-use") (def ^:const docs-link "https://status.im/docs/") + +(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) diff --git a/src/status_im/data_store/activities.cljs b/src/status_im/data_store/activities.cljs index 23238b2bdf..4058b3d1f0 100644 --- a/src/status_im/data_store/activities.cljs +++ b/src/status_im/data_store/activities.cljs @@ -6,16 +6,24 @@ (defn rpc->type [{:keys [type name] :as chat}] (cond - (= 2 type) (assoc chat - :chat-type constants/private-group-chat-type - :chat-name name - :public? false - :group-chat true) - (= 1 type) (assoc chat - :chat-type constants/one-to-one-chat-type - :chat-name name - :public? false - :group-chat false))) + (= constants/activity-center-notification-type-mention type) + (assoc chat + :chat-type constants/private-group-chat-type + :chat-name name) + + (= constants/activity-center-notification-type-private-group-chat type) + (assoc chat + :chat-type constants/private-group-chat-type + :chat-name name + :public? false + :group-chat true) + + (= constants/activity-center-notification-type-one-to-one-chat type) + (assoc chat + :chat-type constants/one-to-one-chat-type + :chat-name name + :public? false + :group-chat false))) (defn <-rpc [item] (-> item @@ -24,4 +32,5 @@ :chatId :chat-id}) (assoc :color (rand-nth colors/chat-colors)) (update :last-message #(when % (messages/<-rpc %))) + (update :message #(when % (messages/<-rpc %))) (dissoc :chatId))) diff --git a/src/status_im/notifications_center/core.cljs b/src/status_im/notifications_center/core.cljs index 20a89e9b9d..14eb99b143 100644 --- a/src/status_im/notifications_center/core.cljs +++ b/src/status_im/notifications_center/core.cljs @@ -6,12 +6,15 @@ [status-im.data-store.activities :as data-store.activities])) (fx/defn handle-activities [{:keys [db]} activities] - (if (= (:view-id db) :notifications-center) - {:db (-> db - (update-in [:activity.center/notifications :notifications] #(concat activities %))) - :dispatch [:mark-all-activity-center-notifications-as-read]} - {:db (-> db - (update :activity.center/notifications-count + (count activities)))})) + {:db (-> db + (update-in [:activity.center/notifications :notifications] #(concat activities %)) + (update :activity.center/notifications-count + (count activities))) + :dispatch (cond + (= (:view-id db) :notifications-center) + [:mark-all-activity-center-notifications-as-read] + + (= (:view-id db) :chat) + [:accept-all-activity-center-notifications-from-chat (:current-chat-id db)])}) (fx/defn get-activity-center-notifications-count {:events [:get-activity-center-notifications-count]} @@ -47,6 +50,22 @@ :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %]) :on-error #()}]}) +(fx/defn accept-all-activity-center-notifications-from-chat + {:events [:accept-all-activity-center-notifications-from-chat]} + [{:keys [db]} chat-id] + (let [notifications (get-in db [:activity.center/notifications :notifications]) + notifications-from-chat (filter #(= chat-id (:chat-id %)) notifications) + ids (map :id notifications-from-chat)] + {:db (-> db + (update-in [:activity.center/notifications :notifications] + (fn [items] (remove #(get ids (:id %)) items))) + (update :activity.center/notifications-count - (count ids))) + ::json-rpc/call [{:method (json-rpc/call-ext-method "acceptActivityCenterNotifications") + :params [ids] + :js-response true + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %]) + :on-error #()}]})) + (fx/defn accept-activity-center-notification-and-open-chat {:events [:accept-activity-center-notification-and-open-chat]} [{:keys [db]} id] @@ -134,7 +153,3 @@ concat (map data-store.activities/<-rpc notifications)))}) -(fx/defn close-center - {:events [:close-notifications-center]} - [cofx] - (clean-notifications cofx)) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 0c2950779a..f80d427edf 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -1681,6 +1681,26 @@ (filter (partial filter-recipient-favs (string/lower-case search-filter)) favs))))) + +;;ACTIVITY CENTER NOTIFICATIONS ======================================================================================== + +(defn- group-notifications-by-date + [notifications] + (->> notifications + (group-by #(datetime/timestamp->date-key (:timestamp %))) + (sort-by key >) + (map (fn [[date-key notifications]] + (let [first-notification (first notifications)] + {:title (clojure.string/capitalize (datetime/day-relative (:timestamp first-notification))) + :key date-key + :data (sort-by :timestamp > notifications)}))))) + +(re-frame/reg-sub + :activity.center/notifications-grouped-by-date + :<- [:activity.center/notifications] + (fn [{:keys [notifications]}] + (group-notifications-by-date (map #(assoc % :timestamp (or (:timestamp %) (:timestamp (or (:message %) (:last-message %))))) notifications)))) + ;;WALLET TRANSACTIONS ================================================================================================== (re-frame/reg-sub diff --git a/src/status_im/ui/screens/communities/community.cljs b/src/status_im/ui/screens/communities/community.cljs index 7b875891ca..dbd5abab24 100644 --- a/src/status_im/ui/screens/communities/community.cljs +++ b/src/status_im/ui/screens/communities/community.cljs @@ -131,7 +131,8 @@ (re-frame/dispatch [:pop-to-root-tab :chat-stack]) (re-frame/dispatch [:dismiss-keyboard]) (re-frame/dispatch [:chat.ui/navigate-to-chat chat-id]) - (re-frame/dispatch [:search/home-filter-changed nil])) + (re-frame/dispatch [:search/home-filter-changed nil]) + (re-frame/dispatch [:accept-all-activity-center-notifications-from-chat chat-id])) :on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet {:content (fn [] [sheets/actions home-item])}])}]) diff --git a/src/status_im/ui/screens/home/views.cljs b/src/status_im/ui/screens/home/views.cljs index 0ee55677a9..272f066870 100644 --- a/src/status_im/ui/screens/home/views.cljs +++ b/src/status_im/ui/screens/home/views.cljs @@ -126,7 +126,8 @@ {:on-press (fn [] (re-frame/dispatch [:dismiss-keyboard]) (re-frame/dispatch [:chat.ui/navigate-to-chat chat-id]) - (re-frame/dispatch [:search/home-filter-changed nil])) + (re-frame/dispatch [:search/home-filter-changed nil]) + (re-frame/dispatch [:accept-all-activity-center-notifications-from-chat chat-id])) :on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet {:content (fn [] [sheets/actions home-item])}])}] diff --git a/src/status_im/ui/screens/notifications_center/styles.cljs b/src/status_im/ui/screens/notifications_center/styles.cljs new file mode 100644 index 0000000000..6dba8971b8 --- /dev/null +++ b/src/status_im/ui/screens/notifications_center/styles.cljs @@ -0,0 +1,66 @@ +(ns status-im.ui.screens.notifications-center.styles + (:require [status-im.ui.components.colors :as colors] + [quo.design-system.colors :as quo-colors])) + +(def notification-message-text + {:flex 1 + :align-self :stretch + :line-height 22 + :font-size 15 + :color (:text-01 @quo-colors/theme)}) + +(def mention-text + {:color colors/blue}) + +(def datetime-text + {:color colors/text-gray + :font-size 10 + :text-align :right + :letter-spacing 0.4 + :align-items :center + :line-height 12 + :position :absolute + :top 17 + :right 16}) + +(def group-info-container + {:height 22 + :align-self :baseline + :align-items :center + :justify-content :center + :border-radius 11 + :border-color colors/gray-transparent-40 + :border-width 1 + :margin-top 6 + :margin-bottom 10 + :padding-left 7 + :padding-right 5 + :flex-direction :row}) + +(defn notification-container [read] + {:min-height 64 + :background-color (when-not read colors/blue-light)}) + +(def notification-content-container + {:flex 1}) + +(def photo-container + {:position :absolute + :top 12 + :left 16}) + +(def title-text + {:margin-left 72 + :margin-top 12 + :margin-right 50}) + +(def notification-message-container + {:margin-left 72 + :margin-right 16}) + +(def group-icon + {:margin-right 4}) + +(def community-info-container + {:flex-direction :row + :align-items :center}) \ No newline at end of file diff --git a/src/status_im/ui/screens/notifications_center/views.cljs b/src/status_im/ui/screens/notifications_center/views.cljs index b69b988592..bc61e4ff19 100644 --- a/src/status_im/ui/screens/notifications_center/views.cljs +++ b/src/status_im/ui/screens/notifications_center/views.cljs @@ -3,39 +3,39 @@ [status-im.ui.components.topbar :as topbar] [status-im.i18n.i18n :as i18n] [status-im.ui.components.list.views :as list] - [status-im.ui.screens.home.views.inner-item :as inner-item] [re-frame.core :as re-frame] [quo.core :as quo] [status-im.ui.components.colors :as colors] [reagent.core :as reagent] [status-im.ui.components.toolbar :as toolbar] - [clojure.string :as string])) + [clojure.string :as string] + [status-im.constants :as constants] + [status-im.ui.screens.notifications-center.views.notification :as notification])) (def selecting (reagent/atom nil)) (def select-all (reagent/atom nil)) (def selected-items (reagent/atom #{})) -(defn render-fn [{:keys [id] :as home-item}] +(defn render-fn [{:keys [id type] :as home-item}] (when id (let [selected (get @selected-items id) on-change (fn [] - (swap! selected-items #(if selected (disj % id) (conj % id))))] + (when-not (= type constants/activity-center-notification-type-mention) (swap! selected-items #(if selected (disj % id) (conj % id)))))] [react/view {:flex-direction :row :flex 1 :align-items :center} - (when @selecting + (when (and @selecting (not (= type constants/activity-center-notification-type-mention))) [react/view {:padding-left 16} [quo/checkbox {:value (or @select-all selected) :disabled @select-all :on-change on-change}]]) [react/view {:flex 1} - [inner-item/home-list-item + [notification/activity-text-item home-item {:on-press (fn [] (if @selecting (on-change) (re-frame/dispatch [:accept-activity-center-notification-and-open-chat id]))) :on-long-press #(do (reset! selecting true) - (swap! selected-items conj id))}]]]))) - + (when-not (= type constants/activity-center-notification-type-mention) (swap! selected-items conj id)))}]]]))) (defn filter-item [] [react/view {:padding-vertical 8 :border-bottom-width 1 :border-bottom-color colors/gray-lighter} [react/view {:align-items :center :justify-content :space-between :padding-horizontal 16 :flex-direction :row} @@ -79,30 +79,42 @@ {:display-name "activity-center" :component-did-mount #(re-frame/dispatch [:get-activity-center-notifications]) :reagent-render (fn [] - (let [{:keys [notifications]} @(re-frame/subscribe [:activity.center/notifications])] + (let [notifications @(re-frame/subscribe [:activity.center/notifications-grouped-by-date])] [react/keyboard-avoiding-view {:style {:flex 1} :ignore-offset true} [topbar/topbar {:navigation {:on-press #(do (reset-state) - (re-frame/dispatch [:close-notifications-center]) (re-frame/dispatch [:navigate-back]))} :title (i18n/label :t/activity)}] - [filter-item] - [list/flat-list - {:key-fn #(or (:chat-id %) (:id %)) - :on-end-reached #(re-frame/dispatch [:load-more-activity-center-notifications]) - :keyboard-should-persist-taps :always - :data notifications - :render-fn render-fn}] - (when (or @select-all (> (count @selected-items) 0)) - [toolbar/toolbar - {:show-border? true - :left [quo/button {:type :secondary - :theme :negative - :accessibility-label :reject-and-delete-activity-center - :on-press #(toolbar-action false)} - (i18n/label :t/reject-and-delete)] - :right [quo/button {:type :secondary - :accessibility-label :accept-and-add-activity-center - :on-press #(toolbar-action true)} - (i18n/label :t/accept-and-add)]}])]))})) + (if (= (count notifications) 0) + [react/view {:style {:flex 1 + :justify-content :center + :align-items :center}} + [quo/text {:color :secondary + :size :large + :align :center} + (i18n/label :t/empty-activity-center)]] + [:<> + [filter-item] + [list/section-list + {:key-fn #(str (:timestamp %) (or (:chat-id %) (:id %))) + :on-end-reached #(re-frame/dispatch [:load-more-activity-center-notifications]) + :keyboard-should-persist-taps :always + :sections notifications + :render-fn render-fn + :stickySectionHeadersEnabled false + :render-section-header-fn + (fn [{:keys [title]}] + [quo/list-header title])}] + (when (or @select-all (> (count @selected-items) 0)) + [toolbar/toolbar + {:show-border? true + :left [quo/button {:type :secondary + :theme :negative + :accessibility-label :reject-and-delete-activity-center + :on-press #(toolbar-action false)} + (i18n/label :t/reject-and-delete)] + :right [quo/button {:type :secondary + :accessibility-label :accept-and-add-activity-center + :on-press #(toolbar-action true)} + (i18n/label :t/accept-and-add)]}])])]))})) diff --git a/src/status_im/ui/screens/notifications_center/views/notification.cljs b/src/status_im/ui/screens/notifications_center/views/notification.cljs new file mode 100644 index 0000000000..a905b8d6fe --- /dev/null +++ b/src/status_im/ui/screens/notifications_center/views/notification.cljs @@ -0,0 +1,165 @@ +(ns status-im.ui.screens.notifications-center.views.notification + (:require [status-im.ui.components.react :as react] + [re-frame.core :as re-frame] + [quo.core :as quo] + [clojure.string :as string] + [status-im.i18n.i18n :as i18n] + [status-im.ui.screens.notifications-center.styles :as styles] + [status-im.utils.handlers :refer [= length max-notification-length) + (reduced acc-paragraph) + (add-parsed-to-message acc-paragraph parsed-child))) + {:components [quo/text] + :length 0} + children) + + "mention" + {:components [quo/text {:style styles/mention-text} [mention-element literal]] + :length 4} ;; we can't predict name length so take the smallest possible + + "status-tag" + (home-item/truncate-literal (str "#" literal)) + + "link" + (home-item/truncate-literal destination) + + (home-item/truncate-literal literal))] + {:components (conj (:components acc) (:components result)) + :length (+ (:length acc) (:length result))})) + +(defn message-wrapper + ([] (message-wrapper 1)) + ([number-of-lines] + [react/text-class {:style styles/notification-message-text + :number-of-lines number-of-lines + :ellipsize-mode :tail + :accessibility-label :chat-message-text}])) + +(defn render-notification-message + "Render the preview of a notification message to a maximum of max-length characters" + ([parsed-text] (render-notification-message parsed-text max-notification-length 1)) + ([parsed-text max-length number-of-lines] + (let [result + (reduce + (fn [{:keys [_ length] :as acc-text} new-text-chunk] + (if (>= length max-length) + (reduced acc-text) + (add-parsed-to-message acc-text new-text-chunk))) + {:components (message-wrapper number-of-lines) + :length 0} + parsed-text)] + (:components result)))) + +(defn message-content-text [{:keys [content content-type community-id]}] + [react/view + (cond + + (not (and content content-type)) + [react/text {:style (merge + styles/notification-message-text + {:color colors/gray}) + :accessibility-label :no-messages-text} + (i18n/label :t/no-messages)] + + (= constants/content-type-sticker content-type) + [react/image {:style {:margin 1 :width 20 :height 20} + ;;TODO (perf) move to event + :source {:uri (contenthash/url (-> content :sticker :hash))}}] + + (= constants/content-type-image content-type) + [react/text {:style styles/notification-message-text + :accessibility-label :no-messages-text} + (i18n/label :t/image)] + + (= constants/content-type-audio content-type) + [react/text {:style styles/notification-message-text + :accessibility-label :no-messages-text} + (i18n/label :t/audio)] + + (= constants/content-type-community content-type) + (let [{:keys [name]} + @(re-frame/subscribe [:communities/community community-id])] + [react/text {:style styles/notification-message-text + :accessibility-label :no-messages-text} + (i18n/label :t/community-message-preview {:community-name name})]) + + (string/blank? (:text content)) + [react/text {:style styles/notification-message-text} + ""] + + (:text content) + (render-notification-message (:parsed-text content) max-notification-length max-notification-lines))]) + +(defn activity-text-item [home-item opts] + (let [{:keys [chat-id chat-name message last-message muted read group-chat timestamp type]} home-item + message (or message last-message) + {:keys [community-id]} (