diff --git a/src/quo2/components/notifications/activity_logs.cljs b/src/quo2/components/notifications/activity_logs.cljs index e499e505dc..31e376d899 100644 --- a/src/quo2/components/notifications/activity_logs.cljs +++ b/src/quo2/components/notifications/activity_logs.cljs @@ -1,10 +1,50 @@ (ns quo2.components.notifications.activity-logs - (:require [react-native.core :as rn] + (:require [quo.core :as quo] [quo2.components.buttons.button :as button] [quo2.components.icon :as icon] [quo2.components.markdown.text :as text] [quo2.components.tags.status-tags :as status-tags] - [quo2.foundations.colors :as colors])) + [quo2.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im.i18n.i18n :as i18n])) + +(def ^:private max-reply-length + 280) + +(defn- valid-reply? + [reply] + (<= (count reply) max-reply-length)) + +(defn- activity-reply-text-input + [reply-input on-update-reply] + [rn/view + [rn/view {:style {:margin-top 16 + :margin-bottom 8 + :flex-direction :row}} + [text/text {:weight :medium + :style {:flex-grow 1 + :color colors/neutral-40}} + (i18n/label :t/your-answer)] + [text/text {:style {:flex-shrink 1 + :color (if (valid-reply? @reply-input) + colors/neutral-40 + colors/danger-60)}} + (str (count @reply-input) "/" max-reply-length)]] + [rn/view + ;; TODO(@ilmotta): Replace with quo2 component when available. + ;; https://github.com/status-im/status-mobile/issues/14364 + [quo/text-input + {:on-change-text #(do (reset! reply-input %) + (when on-update-reply + (on-update-reply %))) + :auto-capitalize :none + :auto-focus true + :accessibility-label :identity-verification-reply-text-input + :placeholder (i18n/label :t/type-something) + :return-key-type :none + :multiline false + :auto-correct false}]]]) (defn- activity-icon [icon] @@ -14,7 +54,6 @@ :margin-top 10 :border-width 1 :border-color colors/white-opa-5 - :flex-direction :column :align-items :center :justify-content :center} [icon/icon icon {:color colors/white}]]) @@ -29,17 +68,17 @@ :border-radius 4}]) (defn- activity-context - [context] - (let [margin-top 4] - (into [rn/view {:flex 1 - :flex-direction :row + [context replying?] + (let [first-line-offset (if replying? 4 -2) + gap-between-lines 4] + (into [rn/view {:flex-direction :row :align-items :center :flex-wrap :wrap - :margin-top (+ 4 (- margin-top))}] + :margin-top first-line-offset}] (map-indexed (fn [index detail] ^{:key index} [rn/view {:margin-right 4 - :margin-top margin-top} + :margin-top gap-between-lines} (if (string? detail) [text/text {:size :paragraph-2} detail] @@ -52,12 +91,11 @@ :margin-top 12 :padding-horizontal 12 :padding-vertical 8 - :background-color colors/white-opa-5 - :flex 1 - :flex-direction :column} + :background-color colors/white-opa-5} (when title [text/text {:size :paragraph-2 - :style {:color colors/white-opa-40}} + :style {:color colors/white-opa-40 + :margin-bottom 2}} title]) (if (string? body) [text/text {:style {:color colors/white} @@ -66,25 +104,24 @@ body)]) (defn- activity-buttons - [button-1 button-2] - (let [size 24 - common-style {:padding-top 3 - :padding-right 8 - :padding-bottom 4 - :padding-left 8}] + [button-1 button-2 replying? reply-input] + (let [size (if replying? 40 24) + common-style (when replying? + {:padding-vertical 9 + :flex-grow 1 + :flex-basis 0})] [rn/view {:margin-top 12 - :flex 1 :flex-direction :row :align-items :flex-start} (when button-1 [button/button (-> button-1 (assoc :size size) - (assoc-in [:style :margin-right] 8) - (update :style merge common-style)) + (update :style merge common-style {:margin-right 8})) (:label button-1)]) (when button-2 [button/button (-> button-2 (assoc :size size) + (assoc :disabled (and replying? (not (valid-reply? @reply-input)))) (update :style merge common-style)) (:label button-2)])])) @@ -98,10 +135,10 @@ :status status}]]) (defn- activity-title - [title] + [title replying?] [text/text {:weight :semi-bold :style {:color colors/white} - :size :paragraph-1} + :size (if replying? :heading-2 :paragraph-1)} title]) (defn- activity-timestamp @@ -112,46 +149,54 @@ :color colors/neutral-40}} timestamp]]) +(defn- footer + [_] + (let [reply-input (reagent/atom "")] + (fn [{:keys [replying? on-update-reply status button-1 button-2]}] + [:<> + (when replying? + [activity-reply-text-input reply-input on-update-reply]) + (cond (some? status) + [activity-status status] + + (or button-1 button-2) + [activity-buttons button-1 button-2 replying? reply-input])]))) + (defn activity-log - [{:keys [button-1 - button-2 - icon + [{:keys [icon message - status context timestamp title - unread?]}] + replying? + unread?] + :as props}] [rn/view {:flex-direction :row - :flex 1 + :align-items :flex-start :border-radius 16 :padding-top 8 - :padding-horizontal 12 + :padding-horizontal (if replying? 20 12) :padding-bottom 12 - :background-color (when unread? + :background-color (when (and unread? (not replying?)) colors/primary-50-opa-10)} - [activity-icon icon] - [rn/view {:flex-direction :column - :padding-left 8 - :flex 1} - [rn/view {:flex 1 + (when-not replying? + [activity-icon icon]) + [rn/view {:padding-left (when-not replying? 8) + :flex-grow 1} + [rn/view {:flex-grow 1 :align-items :center :flex-direction :row} [rn/view {:flex 1 :align-items :center :flex-direction :row} [rn/view {:flex-shrink 1} - [activity-title title]] - [activity-timestamp timestamp]] - (when unread? + [activity-title title replying?]] + (when-not replying? + [activity-timestamp timestamp])] + (when (and unread? (not replying?)) [activity-unread-dot])] (when context - [activity-context context]) + [activity-context context replying?]) (when message [activity-message message]) - (cond - (some? status) - [activity-status status] - - (or button-1 button-2) - [activity-buttons button-1 button-2])]]) + [footer props]]]) diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index f6e7051418..ebe03af3c8 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -32,6 +32,7 @@ quo2.components.notifications.info-count quo2.components.notifications.notification-dot quo2.components.tags.tags + quo2.components.tags.context-tags quo2.components.tabs.tabs quo2.components.tabs.account-selector quo2.components.navigation.top-nav @@ -52,6 +53,7 @@ (def system-message quo2.components.messages.system-message/system-message) (def reaction quo2.components.reactions.reaction/reaction) (def tags quo2.components.tags.tags/tags) +(def user-avatar-tag quo2.components.tags.context-tags/user-avatar-tag) (def tabs quo2.components.tabs.tabs/tabs) (def scrollable-tabs quo2.components.tabs.tabs/scrollable-tabs) (def account-selector quo2.components.tabs.account-selector/account-selector) @@ -88,4 +90,4 @@ ;;;; NOTIFICATIONS (def activity-log quo2.components.notifications.activity-logs/activity-log) (def info-count quo2.components.notifications.info-count/info-count) -(def notification-dot quo2.components.notifications.notification-dot/notification-dot) \ No newline at end of file +(def notification-dot quo2.components.notifications.notification-dot/notification-dot) diff --git a/src/status_im/activity_center/core.cljs b/src/status_im/activity_center/core.cljs index c677e16d8a..a793c4731c 100644 --- a/src/status_im/activity_center/core.cljs +++ b/src/status_im/activity_center/core.cljs @@ -6,6 +6,14 @@ [status-im.utils.fx :as fx] [taoensso.timbre :as log])) +;;;; Misc + +(fx/defn process-notification-failure + {:events [:activity-center/process-notification-failure]} + [_ notification-id action error] + (log/warn (str "Failed to " action) + {:notification-id notification-id :error error})) + ;;;; Notification reconciliation (defn- update-notifications @@ -45,38 +53,69 @@ {:db (update-in db [:activity-center :notifications] update-notifications new-notifications)})) -;;;; Contact verification - -(fx/defn contact-verification-decline - {:events [:activity-center.contact-verification/decline]} - [_ contact-verification-id] - {::json-rpc/call [{:method "wakuext_declineContactVerificationRequest" - :params [contact-verification-id] - :on-success #(rf/dispatch [:activity-center.contact-verification/decline-success %]) - :on-error #(rf/dispatch [:activity-center.contact-verification/decline-error contact-verification-id %])}]}) - -(fx/defn contact-verification-decline-success - {:events [:activity-center.contact-verification/decline-success]} +(fx/defn notifications-reconcile-from-response + {:events [:activity-center/reconcile-notifications-from-response]} [cofx response] (->> response :activityCenterNotifications (map data-store.activities/<-rpc) (notifications-reconcile cofx))) -(fx/defn contact-verification-decline-error - {:events [:activity-center.contact-verification/decline-error]} - [_ contact-verification-id error] - (log/warn "Failed to decline contact verification" - {:contact-verification-id contact-verification-id - :error error}) - nil) +;;;; Contact verification + +(fx/defn contact-verification-decline + {:events [:activity-center.contact-verification/decline]} + [_ notification-id] + {::json-rpc/call [{:method "wakuext_declineContactVerificationRequest" + :params [notification-id] + :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response %]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :contact-verification/decline + %])}]}) + +(fx/defn contact-verification-reply + {:events [:activity-center.contact-verification/reply]} + [_ notification-id reply] + {::json-rpc/call [{:method "wakuext_acceptContactVerificationRequest" + :params [notification-id reply] + :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response %]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :contact-verification/reply + %])}]}) + +(fx/defn contact-verification-mark-as-trusted + {:events [:activity-center.contact-verification/mark-as-trusted]} + [_ notification-id] + {::json-rpc/call [{:method "wakuext_verifiedTrusted" + :params [{:id notification-id}] + :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response %]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :contact-verification/mark-as-trusted + %])}]}) + +(fx/defn contact-verification-mark-as-untrustworthy + {:events [:activity-center.contact-verification/mark-as-untrustworthy]} + [_ notification-id] + {::json-rpc/call [{:method "wakuext_verifiedUntrustworthy" + :params [{:id notification-id}] + :on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response %]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :contact-verification/mark-as-untrustworthy + %])}]}) ;;;; Notifications fetching and pagination (def defaults {:filter-status :unread :filter-type types/no-type - :notifications-per-page 10}) + ;; Choose the maximum number of notifications that *usually/safely* fit on + ;; most screens, so that the UI doesn't have to needlessly render + ;; notifications. + :notifications-per-page 8}) (def start-or-end-cursor "") @@ -92,9 +131,9 @@ (defn status [filter-status] (case filter-status - :read status-read + :read status-read :unread status-unread - :all status-all + :all status-all 99)) (fx/defn notifications-fetch diff --git a/src/status_im/activity_center/core_test.cljs b/src/status_im/activity_center/core_test.cljs index 8f7da7d756..334dedad24 100644 --- a/src/status_im/activity_center/core_test.cljs +++ b/src/status_im/activity_center/core_test.cljs @@ -3,6 +3,7 @@ [day8.re-frame.test :as rf-test] [re-frame.core :as rf] [status-im.activity-center.notification-types :as types] + [status-im.constants :as constants] [status-im.ethereum.json-rpc :as json-rpc] status-im.events [status-im.test-helpers :as h] @@ -14,87 +15,140 @@ ;;;; Contact verification +(def notification-id 24) + +(def contact-verification-rpc-response + {:activityCenterNotifications + [{:accepted false + :author "0x04d03f" + :chatId "0x04d03f" + :contactVerificationStatus constants/contact-verification-status-pending + :dismissed false + :id notification-id + :message {} + :name "0x04d03f" + :read true + :timestamp 1666647286000 + :type types/contact-verification}]}) + +(def contact-verification-expected-notification + {:accepted false + :author "0x04d03f" + :chat-id "0x04d03f" + :contact-verification-status constants/contact-verification-status-pending + :dismissed false + :id notification-id + :last-message nil + :message {:command-parameters nil + :content {:chat-id nil + :ens-name nil + :image nil + :line-count nil + :links nil + :parsed-text nil + :response-to nil + :rtl? nil + :sticker nil + :text nil} + :outgoing false + :outgoing-status nil + :quoted-message nil} + :name "0x04d03f" + :read true + :reply-message nil + :timestamp 1666647286000 + :type types/contact-verification}) + +(defn test-log-on-failure + [{:keys [notification-id event action]}] + (rf-test/run-test-sync + (setup) + (h/using-log-test-appender + (fn [logs] + (h/stub-fx-with-callbacks ::json-rpc/call :on-error (constantly :fake-error)) + + (rf/dispatch event) + + (is (= {:args [(str "Failed to " action) + {:notification-id notification-id + :error :fake-error}] + :level :warn} + (last @logs))))))) + +(defn test-contact-verification-event + [{:keys [event expected-rpc-call]}] + (rf-test/run-test-sync + (setup) + (let [spy-queue (atom [])] + (h/stub-fx-with-callbacks ::json-rpc/call :on-success (constantly contact-verification-rpc-response)) + (h/spy-fx spy-queue ::json-rpc/call) + (rf/dispatch event) + + (is (= {types/no-type + {:read {:data [contact-verification-expected-notification]} + :unread {:data []}} + types/contact-verification + {:read {:data [contact-verification-expected-notification]} + :unread {:data []}}} + (get-in (h/db) [:activity-center :notifications]))) + + (is (= expected-rpc-call + (-> @spy-queue + (get-in [0 :args 0]) + (select-keys [:method :params]))))))) + (deftest contact-verification-decline-test (with-redefs [config/new-activity-center-enabled? true] - (testing "successfully declines and reconciles returned notification" - (rf-test/run-test-sync - (setup) - (let [spy-queue (atom []) - contact-verification-id 24 - expected-notification {:accepted false - :author "0x04d03f" - :chat-id "0x04d03f" - :contact-verification-status 3 - :dismissed false - :id 24 - :last-message nil - :message {:command-parameters nil - :content {:chat-id nil - :ens-name nil - :image nil - :line-count nil - :links nil - :parsed-text nil - :response-to nil - :rtl? nil - :sticker nil - :text nil} - :outgoing false - :outgoing-status nil - :quoted-message nil} - :name "0x04d03f" - :read true - :reply-message nil - :timestamp 1666647286000 - :type types/contact-verification}] - (h/stub-fx-with-callbacks - ::json-rpc/call - :on-success (constantly {:activityCenterNotifications - [{:accepted false - :author "0x04d03f" - :chatId "0x04d03f" - :contactVerificationStatus 3 - :dismissed false - :id contact-verification-id - :message {} - :name "0x04d03f" - :read true - :timestamp 1666647286000 - :type types/contact-verification}]})) + (testing "declines notification and reconciles" + (test-contact-verification-event + {:event [:activity-center.contact-verification/decline notification-id] + :expected-rpc-call {:method "wakuext_declineContactVerificationRequest" + :params [notification-id]}})) + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.contact-verification/decline notification-id] + :action :contact-verification/decline})))) - (h/spy-fx spy-queue ::json-rpc/call) +(deftest contact-verification-reply-test + (with-redefs [config/new-activity-center-enabled? true] + (testing "sends reply and reconciles" + (let [reply "any answer"] + (test-contact-verification-event + {:event [:activity-center.contact-verification/reply notification-id reply] + :expected-rpc-call {:method "wakuext_acceptContactVerificationRequest" + :params [notification-id reply]}}))) + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.contact-verification/reply notification-id "any answer"] + :action :contact-verification/reply})))) - (rf/dispatch [:activity-center.contact-verification/decline contact-verification-id]) +(deftest contact-verification-mark-as-trusted-test + (with-redefs [config/new-activity-center-enabled? true] + (testing "marks notification as trusted and reconciles" + (test-contact-verification-event + {:event [:activity-center.contact-verification/mark-as-trusted notification-id] + :expected-rpc-call {:method "wakuext_verifiedTrusted" + :params [{:id notification-id}]}})) + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.contact-verification/mark-as-trusted notification-id] + :action :contact-verification/mark-as-trusted})))) - (is (= {:method "wakuext_declineContactVerificationRequest" - :params [contact-verification-id]} - (-> @spy-queue - (get-in [0 :args 0]) - (select-keys [:method :params])))) - - (is (= {types/no-type - {:read {:data [expected-notification]} - :unread {:data []}} - types/contact-verification - {:read {:data [expected-notification]} - :unread {:data []}}} - (get-in (h/db) [:activity-center :notifications])))))) - - (testing "logs failure" - (rf-test/run-test-sync - (setup) - (let [contact-verification-id 666] - (h/using-log-test-appender - (fn [logs] - (h/stub-fx-with-callbacks ::json-rpc/call :on-error (constantly :fake-error)) - - (rf/dispatch [:activity-center.contact-verification/decline contact-verification-id]) - - (is (= {:args ["Failed to decline contact verification" - {:contact-verification-id contact-verification-id - :error :fake-error}] - :level :warn} - (last @logs)))))))))) +(deftest contact-verification-mark-as-untrustworthy-test + (with-redefs [config/new-activity-center-enabled? true] + (testing "marks notification as untrustworthy and reconciles" + (test-contact-verification-event + {:event [:activity-center.contact-verification/mark-as-untrustworthy notification-id] + :expected-rpc-call {:method "wakuext_verifiedUntrustworthy" + :params [{:id notification-id}]}})) + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.contact-verification/mark-as-untrustworthy notification-id] + :action :contact-verification/mark-as-untrustworthy})))) ;;;; Notification reconciliation diff --git a/src/status_im/activity_center/notification_types.cljs b/src/status_im/activity_center/notification_types.cljs index 49a42db3a5..da000a11db 100644 --- a/src/status_im/activity_center/notification_types.cljs +++ b/src/status_im/activity_center/notification_types.cljs @@ -6,7 +6,7 @@ (def ^:const mention 3) (def ^:const reply 4) (def ^:const contact-request 5) -(def ^:const contact-verification 6) +(def ^:const contact-verification 10) ;; TODO: Remove this constant once the old Notification Center code is removed. ;; Its value clashes with the new constant `-contact-verification` diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index cf7dc9640a..77e5447989 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -21,12 +21,13 @@ (def ^:const contact-request-state-received 3) (def ^:const contact-request-state-dismissed 4) -(def ^:const contact-verification-state-unknown 0) -(def ^:const contact-verification-state-pending 1) -(def ^:const contact-verification-state-accepted 2) -(def ^:const contact-verification-state-declined 3) -(def ^:const contact-verification-state-cancelled 4) -(def ^:const contact-verification-state-trusted 5) +(def ^:const contact-verification-status-unknown 0) +(def ^:const contact-verification-status-pending 1) +(def ^:const contact-verification-status-accepted 2) +(def ^:const contact-verification-status-declined 3) +(def ^:const contact-verification-status-cancelled 4) +(def ^:const contact-verification-status-trusted 5) +(def ^:const contact-verification-status-untrustworthy 6) (def ^:const emoji-reaction-love 1) (def ^:const emoji-reaction-thumbs-up 2) diff --git a/src/status_im/data_store/activities_test.cljs b/src/status_im/data_store/activities_test.cljs index 33a66e889c..adde816019 100644 --- a/src/status_im/data_store/activities_test.cljs +++ b/src/status_im/data_store/activities_test.cljs @@ -13,7 +13,7 @@ (def raw-notification {:chatId chat-id - :contactVerificationStatus constants/contact-verification-state-pending + :contactVerificationStatus constants/contact-verification-status-pending :lastMessage {} :name chat-name :replyMessage {}}) @@ -23,7 +23,7 @@ (testing "renames keys" (is (= {:name chat-name :chat-id chat-id - :contact-verification-status constants/contact-verification-state-pending} + :contact-verification-status constants/contact-verification-status-pending} (-> raw-notification store/<-rpc (dissoc :last-message :message :reply-message))))) diff --git a/src/status_im/ui/screens/activity_center/notification/contact_request/style.cljs b/src/status_im/ui/screens/activity_center/notification/contact_request/style.cljs new file mode 100644 index 0000000000..90a6ad4c80 --- /dev/null +++ b/src/status_im/ui/screens/activity_center/notification/contact_request/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.ui.screens.activity-center.notification.contact-request.style + (:require [quo2.foundations.colors :as colors])) + +(def context-tag-text + {:color colors/white}) + +(def user-avatar-tag-text + {:color colors/white}) + +(def user-avatar-tag + {:background-color colors/white-opa-10}) diff --git a/src/status_im/ui/screens/activity_center/notification/contact_request/view.cljs b/src/status_im/ui/screens/activity_center/notification/contact_request/view.cljs new file mode 100644 index 0000000000..2f3f307c7b --- /dev/null +++ b/src/status_im/ui/screens/activity_center/notification/contact_request/view.cljs @@ -0,0 +1,58 @@ +(ns status-im.ui.screens.activity-center.notification.contact-request.view + (:require [quo.components.animated.pressable :as animation] + [quo2.core :as quo2] + [status-im.constants :as constants] + [status-im.i18n.i18n :as i18n] + [status-im.multiaccounts.core :as multiaccounts] + [status-im.ui.screens.activity-center.notification.contact-request.style :as style] + [status-im.ui.screens.activity-center.utils :as activity-center.utils] + [status-im.utils.datetime :as datetime] + [utils.re-frame :as rf])) + +(defn view + [{:keys [id author message last-message] :as notification}] + (let [message (or message last-message) + contact (rf/sub [:contacts/contact-by-identity author]) + pressable (case (:contact-request-state message) + constants/contact-request-message-state-accepted + ;; NOTE(2022-09-21): We need to dispatch to + ;; `:contact.ui/send-message-pressed` instead of + ;; `: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 (fn [] + (rf/dispatch [:hide-popover]) + (rf/dispatch [:contact.ui/send-message-pressed {:public-key author}]))}] + [:<>])] + (conj pressable + [quo2/activity-log + (merge {:title (i18n/label :t/contact-request) + :icon :main-icons2/add-user + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :context [[quo2/user-avatar-tag + {:color :purple + :override-theme :dark + :size :small + :style style/user-avatar-tag + :text-style style/user-avatar-tag-text} + (activity-center.utils/contact-name contact) + (multiaccounts/displayed-photo contact)] + [quo2/text {:style style/context-tag-text} + (i18n/label :t/contact-request-sent)]] + :message {:body (get-in message [:content :text])} + :status (case (:contact-request-state message) + constants/contact-request-message-state-accepted + {:type :positive :label (i18n/label :t/accepted)} + constants/contact-request-message-state-declined + {:type :negative :label (i18n/label :t/declined)} + nil)} + (case (:contact-request-state message) + constants/contact-request-state-mutual + {:button-1 {:label (i18n/label :t/decline) + :type :danger + :on-press #(rf/dispatch [:contact-requests.ui/decline-request id])} + :button-2 {:label (i18n/label :t/accept) + :type :positive + :on-press #(rf/dispatch [:contact-requests.ui/accept-request id])}} + nil))]))) diff --git a/src/status_im/ui/screens/activity_center/notification/contact_verification/style.cljs b/src/status_im/ui/screens/activity_center/notification/contact_verification/style.cljs new file mode 100644 index 0000000000..bc1ffb8781 --- /dev/null +++ b/src/status_im/ui/screens/activity_center/notification/contact_verification/style.cljs @@ -0,0 +1,11 @@ +(ns status-im.ui.screens.activity-center.notification.contact-verification.style + (:require [quo2.foundations.colors :as colors])) + +(def context-tag-text + {:color colors/white}) + +(def user-avatar-tag-text + {:color colors/white}) + +(def user-avatar-tag + {:background-color colors/white-opa-10}) diff --git a/src/status_im/ui/screens/activity_center/notification/contact_verification/view.cljs b/src/status_im/ui/screens/activity_center/notification/contact_verification/view.cljs new file mode 100644 index 0000000000..cbb2099829 --- /dev/null +++ b/src/status_im/ui/screens/activity_center/notification/contact_verification/view.cljs @@ -0,0 +1,105 @@ +(ns status-im.ui.screens.activity-center.notification.contact-verification.view + (:require [clojure.string :as str] + [quo2.core :as quo2] + [status-im.constants :as constants] + [status-im.i18n.i18n :as i18n] + [status-im.multiaccounts.core :as multiaccounts] + [status-im.ui.screens.activity-center.notification.contact-verification.style :as style] + [status-im.ui.screens.activity-center.utils :as activity-center.utils] + [status-im.utils.datetime :as datetime] + [utils.re-frame :as rf])) + +(defn- hide-bottom-sheet-and-dispatch + [event] + (rf/dispatch [:bottom-sheet/hide]) + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch event)) + +(defn- context-tags + [challenger? {:keys [author contact-verification-status]}] + (let [contact (rf/sub [:contacts/contact-by-identity author])] + [[quo2/user-avatar-tag + {:color :purple + :override-theme :dark + :size :small + :style style/user-avatar-tag + :text-style style/user-avatar-tag-text} + (activity-center.utils/contact-name contact) + (multiaccounts/displayed-photo contact)] + [quo2/text {:style style/context-tag-text} + (if challenger? + (cond (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-trusted) + (= contact-verification-status constants/contact-verification-status-untrustworthy)) + (str (str/lower-case (i18n/label :t/replied)) ":")) + (cond (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-pending) + (= contact-verification-status constants/contact-verification-status-declined)) + (str (i18n/label :t/identity-verification-request-sent) ":")))]])) + +(defn- activity-message + [challenger? {:keys [contact-verification-status message reply-message]}] + (if challenger? + (cond (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-trusted) + (= contact-verification-status constants/contact-verification-status-untrustworthy)) + {:title (get-in message [:content :text]) + :body (get-in reply-message [:content :text])}) + (cond (or (= contact-verification-status constants/contact-verification-status-accepted) + (= contact-verification-status constants/contact-verification-status-pending) + (= contact-verification-status constants/contact-verification-status-declined)) + {:body (get-in message [:content :text])}))) + +(defn- activity-status + [challenger? contact-verification-status] + (if challenger? + (cond (= contact-verification-status constants/contact-verification-status-trusted) + {:type :positive :label (i18n/label :t/status-confirmed)} + (= contact-verification-status constants/contact-verification-status-untrustworthy) + {:type :negative :label (i18n/label :t/untrustworthy)}) + (cond (= contact-verification-status constants/contact-verification-status-accepted) + {:type :positive :label (i18n/label :t/replied)} + (= contact-verification-status constants/contact-verification-status-declined) + {:type :negative :label (i18n/label :t/declined)}))) + +(defn view + [_ _] + (let [reply (atom "")] + (fn [{:keys [id message contact-verification-status] :as notification} {:keys [replying?]}] + (let [challenger? (:outgoing message)] + ;; TODO(@ilmotta): Declined challenges should only be displayed for the + ;; challengee, not the challenger. + ;; https://github.com/status-im/status-mobile/issues/14354 + (when-not (and challenger? (= contact-verification-status constants/contact-verification-status-declined)) + [quo2/activity-log + (merge {:title (i18n/label :t/identity-verification-request) + :icon :i/friend + :timestamp (datetime/timestamp->relative (:timestamp notification)) + :unread? (not (:read notification)) + :on-update-reply #(reset! reply %) + :replying? replying? + :context (context-tags challenger? notification) + :message (activity-message challenger? notification) + :status (activity-status challenger? contact-verification-status)} + (if challenger? + (cond (= contact-verification-status constants/contact-verification-status-accepted) + {:button-1 {:label (i18n/label :t/untrustworthy) + :type :danger + :on-press #(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy id])} + :button-2 {:label (i18n/label :t/accept) + :type :positive + :on-press #(rf/dispatch [:activity-center.contact-verification/mark-as-trusted id])}}) + (cond (= contact-verification-status constants/contact-verification-status-pending) + {:button-1 {:label (i18n/label :t/decline) + :type :danger + :on-press #(hide-bottom-sheet-and-dispatch [:activity-center.contact-verification/decline id])} + :button-2 (if replying? + {:label (i18n/label :t/send-reply) + :type :primary + :on-press #(hide-bottom-sheet-and-dispatch [:activity-center.contact-verification/reply id @reply])} + {:label (i18n/label :t/message-reply) + :type :primary + :on-press #(rf/dispatch [:bottom-sheet/show-sheet + :activity-center.contact-verification/reply + {:notification notification + :replying? true}])})})))]))))) diff --git a/src/status_im/ui/screens/activity_center/sheet/contact_verification.cljs b/src/status_im/ui/screens/activity_center/sheet/contact_verification.cljs new file mode 100644 index 0000000000..aab46f4eac --- /dev/null +++ b/src/status_im/ui/screens/activity_center/sheet/contact_verification.cljs @@ -0,0 +1,9 @@ +(ns status-im.ui.screens.activity-center.sheet.contact-verification + (:require [status-im.ui.screens.activity-center.notification.contact-verification.view :as contact-verification])) + +(defn- reply-view + [{:keys [notification replying?]}] + [contact-verification/view notification {:replying? replying?}]) + +(def reply + {:content reply-view}) diff --git a/src/status_im/ui/screens/activity_center/style.cljs b/src/status_im/ui/screens/activity_center/style.cljs new file mode 100644 index 0000000000..3fb6a8454c --- /dev/null +++ b/src/status_im/ui/screens/activity_center/style.cljs @@ -0,0 +1,31 @@ +(ns status-im.ui.screens.activity-center.style + (:require [quo2.foundations.colors :as colors])) + +(def screen-padding 20) + +(def header-button + {:margin-bottom 12 + :margin-left screen-padding}) + +(def header-heading + {:padding-horizontal screen-padding + :padding-vertical 12 + :color colors/white}) + +(defn screen-container + [window-width top bottom] + {:flex 1 + :width window-width + :padding-top (if (pos? 0) (+ top 12) 12) + :padding-bottom bottom}) + +(def notifications-container + {:flex-grow 1}) + +(defn notification-container + [index] + {:margin-top (if (zero? index) 0 4) + :padding-horizontal screen-padding}) + +(def tabs + {:padding-left screen-padding}) diff --git a/src/status_im/ui/screens/activity_center/utils.cljs b/src/status_im/ui/screens/activity_center/utils.cljs new file mode 100644 index 0000000000..60a443dbbf --- /dev/null +++ b/src/status_im/ui/screens/activity_center/utils.cljs @@ -0,0 +1,6 @@ +(ns status-im.ui.screens.activity-center.utils) + +(defn contact-name + [contact] + (or (get-in contact [:names :nickname]) + (get-in contact [:names :three-words-name]))) diff --git a/src/status_im/ui/screens/activity_center/views.cljs b/src/status_im/ui/screens/activity_center/views.cljs index 117d783fe3..7c98c5e109 100644 --- a/src/status_im/ui/screens/activity_center/views.cljs +++ b/src/status_im/ui/screens/activity_center/views.cljs @@ -1,154 +1,32 @@ (ns status-im.ui.screens.activity-center.views - (:require [quo.components.animated.pressable :as animation] + (:require [quo.components.safe-area :as safe-area] [quo.react :as react] [quo.react-native :as rn] - [quo2.components.buttons.button :as button] - [quo2.components.markdown.text :as text] - [quo2.components.notifications.activity-logs :as activity-logs] - [quo2.components.tabs.tabs :as tabs] - [quo2.components.tags.context-tags :as context-tags] + [quo2.core :as quo2] [quo2.foundations.colors :as colors] - [status-im.constants :as constants] [status-im.activity-center.notification-types :as types] [status-im.i18n.i18n :as i18n] - [status-im.multiaccounts.core :as multiaccounts] - [status-im.utils.datetime :as datetime] - [goog.string :as gstring] - [status-im.utils.handlers :refer [evt]] - [quo.components.safe-area :as safe-area])) - -;;;; Misc - -(defn sender-name - [contact] - (or (get-in contact [:names :nickname]) - (get-in contact [:names :three-words-name]))) - -(defmulti notification-component :type) - -;; TODO(rasom): should be removed as soon as all notifications types covered -(defmethod notification-component :default - [{:keys [type]}] - [rn/view {:style {:width 300, :height 100}} - [rn/text - (gstring/format - "I exist just to avoid crashing for no reason. I'm sorry. Type %d" type)]]) - -;;;; Contact request notifications - -(defmethod notification-component types/contact-request - [{:keys [id] :as notification}] - (let [message (or (:message notification) (:last-message notification)) - contact (evt [:hide-popover]) - (>evt [:contact.ui/send-message-pressed {:public-key (:author notification)}]))}] - [:<>])] - (conj pressable - [activity-logs/activity-log - (merge {:title (i18n/label :t/contact-request) - :icon :i/add-user - :timestamp (datetime/timestamp->relative (:timestamp notification)) - :unread? (not (:read notification)) - :context [[context-tags/user-avatar-tag - {:color :purple - :override-theme :dark - :size :small - :style {:background-color colors/white-opa-10} - :text-style {:color colors/white}} - (sender-name contact) - (multiaccounts/displayed-photo contact)] - [rn/text {:style {:color colors/white}} - (i18n/label :t/contact-request-sent)]] - :message {:body (get-in message [:content :text])} - :status (case (:contact-request-state message) - constants/contact-request-message-state-accepted - {:type :positive :label (i18n/label :t/accepted)} - constants/contact-request-message-state-declined - {:type :negative :label (i18n/label :t/declined)} - nil)} - (case (:contact-request-state message) - constants/contact-request-state-mutual - {:button-1 {:label (i18n/label :t/decline) - :type :danger - :on-press #(>evt [:contact-requests.ui/decline-request id])} - :button-2 {:label (i18n/label :t/message-reply) - :type :success - :override-background-color colors/success-60 - :on-press #(>evt [:contact-requests.ui/accept-request id])}} - nil))]))) - -;;;; Contact verification notifications - -(defmethod notification-component types/contact-verification - [{:keys [id contact-verification-status] :as notification}] - (let [message (or (:message notification) (:last-notification notification)) - contact (relative (:timestamp notification)) - :unread? (not (:read notification)) - :context [[context-tags/user-avatar-tag - {:color :purple - :override-theme :dark - :size :small - :style {:background-color colors/white-opa-10} - :text-style {:color colors/white}} - (sender-name contact) - (multiaccounts/displayed-photo contact)] - [rn/text {:style {:color colors/white}} - (str (i18n/label :t/identity-verification-request-sent) - ":")]] - :message (case contact-verification-status - (constants/contact-verification-state-pending - constants/contact-verification-state-declined) - {:body (get-in message [:content :text])} - nil) - :status (case contact-verification-status - constants/contact-verification-state-declined - {:type :negative :label (i18n/label :t/declined)} - nil)} - (case contact-verification-status - constants/contact-verification-state-pending - {:button-1 {:label (i18n/label :t/decline) - :type :danger - :on-press #(>evt [:activity-center.contact-verification/decline id])} - :button-2 {:label (i18n/label :t/accept) - :type :primary - ;; TODO: The acceptance flow will be implemented in follow-up PRs. - :on-press identity}} - nil))])) - -;;;; Type-independent components - -(defn render-notification - [notification index] - [rn/view {:margin-top (if (= 0 index) 0 4) - :padding-horizontal 20} - [notification-component notification]]) + [status-im.ui.screens.activity-center.notification.contact-request.view :as contact-request] + [status-im.ui.screens.activity-center.notification.contact-verification.view :as contact-verification] + [status-im.ui.screens.activity-center.style :as style] + [utils.re-frame :as rf])) (defn filter-selector-read-toggle [] - (let [unread-filter-enabled? (evt [:activity-center.notifications/fetch-first-page - {:filter-status (if unread-filter-enabled? - :all - :unread)}])} + (let [unread-filter-enabled? (rf/sub [:activity-center/filter-status-unread-enabled?])] + ;; TODO(@ilmotta): Replace the button by a Filter Selector. + ;; https://github.com/status-im/status-mobile/issues/14355 + [quo2/button {:icon true + :type (if unread-filter-enabled? :primary :blur-bg-outline) + :size 32 + :override-theme :dark + :on-press #(rf/dispatch [:activity-center.notifications/fetch-first-page + {:filter-status (if unread-filter-enabled? + :all + :unread)}])} :i/unread])) -;; TODO(2022-10-07): The empty state is still under design analysis, so we +;; TODO(@ilmotta,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 @@ -161,24 +39,24 @@ :height 120 :margin-bottom 20 :width 120}}] - [text/text {:size :paragraph-1 + [quo2/text {:size :paragraph-1 :style {:padding-bottom 2} :weight :semi-bold} "No notifications"] - [text/text {:size :paragraph-2} + [quo2/text {:size :paragraph-2} "Your notifications will be here"]]) (defn tabs [] - (let [filter-type (evt [:activity-center.notifications/fetch-first-page {:filter-type %}]) + :on-change #(rf/dispatch [:activity-center.notifications/fetch-first-page {:filter-type %}]) :default-active filter-type :data [{:id types/no-type :label (i18n/label :t/all)} @@ -203,19 +81,16 @@ [] (let [screen-padding 20] [rn/view - [button/button {:icon true - :type :blur-bg - :size 32 - :override-theme :dark - :style {:margin-bottom 12 - :margin-left screen-padding} - :on-press #(>evt [:hide-popover])} + [quo2/button {:icon true + :type :blur-bg + :size 32 + :override-theme :dark + :style style/header-button + :on-press #(rf/dispatch [:hide-popover])} :i/close] - [text/text {:size :heading-1 + [quo2/text {:size :heading-1 :weight :semi-bold - :style {:padding-horizontal screen-padding - :padding-vertical 12 - :color colors/white}} + :style style/header-heading} (i18n/label :t/notifications)] [rn/view {:flex-direction :row :padding-vertical 12} @@ -227,22 +102,31 @@ :padding-right screen-padding} [filter-selector-read-toggle]]]])) +(defn render-notification + [notification index] + [rn/view {:style (style/notification-container index)} + (case (:type notification) + types/contact-verification + [contact-verification/view notification {}] + + types/contact-request + [contact-request/view notification] + + nil)]) + (defn activity-center [] [:f> (fn [] - (let [notifications (evt [:activity-center.notifications/fetch-first-page])) - [rn/view {:style {:flex 1 - :width window-width - :padding-top (if (pos? top) (+ top 12) 12) - :padding-bottom bottom}} + (let [notifications (rf/sub [:activity-center/filtered-notifications]) + window-width (rf/sub [:dimensions/window-width]) + {:keys [top bottom]} (safe-area/use-safe-area)] + (react/effect! #(rf/dispatch [:activity-center.notifications/fetch-first-page])) + [rn/view {:style (style/screen-container window-width top bottom)} [header] - [rn/flat-list {:content-container-style {:flex-grow 1} + [rn/flat-list {:content-container-style style/notifications-container :data notifications :empty-component [empty-tab] :key-fn :id - :on-end-reached #(>evt [:activity-center.notifications/fetch-next-page]) + :on-end-reached #(rf/dispatch [:activity-center.notifications/fetch-next-page]) :render-fn render-notification}]]))]) diff --git a/src/status_im/ui/screens/bottom_sheets/views.cljs b/src/status_im/ui/screens/bottom_sheets/views.cljs index d6174cc794..b7cd6202b2 100644 --- a/src/status_im/ui/screens/bottom_sheets/views.cljs +++ b/src/status_im/ui/screens/bottom_sheets/views.cljs @@ -1,12 +1,13 @@ (ns status-im.ui.screens.bottom-sheets.views - (:require [status-im.ui.screens.mobile-network-settings.view :as mobile-network-settings] + (:require [quo.core :as quo] [re-frame.core :as re-frame] + [status-im.ui.screens.about-app.views :as about-app] + [status-im.ui.screens.activity-center.sheet.contact-verification :as contact-verification.sheet] [status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.keycard.views :as keycard] + [status-im.ui.screens.mobile-network-settings.view :as mobile-network-settings] [status-im.ui.screens.multiaccounts.key-storage.views :as key-storage] - [status-im.ui.screens.about-app.views :as about-app] - [status-im.ui.screens.multiaccounts.recover.views :as recover.views] - [quo.core :as quo])) + [status-im.ui.screens.multiaccounts.recover.views :as recover.views])) (defn bottom-sheet [] (let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet]) @@ -27,6 +28,9 @@ (= view :add-new) (merge home.sheet/add-new) + (= view :activity-center.contact-verification/reply) + (merge contact-verification.sheet/reply) + (= view :keycard.login/more) (merge keycard/more-sheet) @@ -40,4 +44,4 @@ (merge key-storage/migrate-account-password))] [quo/bottom-sheet opts (when content - [content (when options options)])])) \ No newline at end of file + [content (when options options)])])) diff --git a/src/status_im/ui2/screens/quo2_preview/notifications/activity_logs.cljs b/src/status_im/ui2/screens/quo2_preview/notifications/activity_logs.cljs index 7d9a284c59..ea1a1d4e1a 100644 --- a/src/status_im/ui2/screens/quo2_preview/notifications/activity_logs.cljs +++ b/src/status_im/ui2/screens/quo2_preview/notifications/activity_logs.cljs @@ -11,6 +11,9 @@ (def descriptor [{:label "Unread?" :key :unread? :type :boolean} + {:label "Replying?" + :key :replying? + :type :boolean} {:label "Icon" :key :icon :type :select @@ -138,6 +141,10 @@ (= (:message @state) :with-mention) (assoc :message message-with-mention) + (some? (:status @state)) + (update :status (fn [status] + {:label (name status) :type status})) + (= (:message @state) :with-title) (assoc :message message-with-title) diff --git a/status-go-version.json b/status-go-version.json index 49430dea3a..79d6c3f96b 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -3,7 +3,7 @@ "_comment": "Instead use: scripts/update-status-go.sh ", "owner": "status-im", "repo": "status-go", - "version": "v0.115.0", - "commit-sha1": "2341dedfbabfb8e392aaa49ecb9bb7fab6a74b99", - "src-sha256": "1x8r3rln2vna82kkvl42q11wh7zn2acr3crzj63pzcawdsr6l990" + "version": "v0.115.1", + "commit-sha1": "2572321063182c0a0376da8a3462544e598c0f9d", + "src-sha256": "07d996c10z4acin5wk21qyh9j1k1sc75a7x5d5hffaxllha7m9rs" } diff --git a/translations/en.json b/translations/en.json index 5faabc7fe8..c495c311c2 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1138,6 +1138,7 @@ "send-logs-to": "Report a bug to {{email}}", "send-message": "Send message", "send-request": "Send request", + "send-reply": "Send reply", "send-request-amount": "Amount", "send-request-amount-max-decimals": "Max number of decimals is {{asset-decimals}}", "send-request-unknown-token": "Unknown token - {{asset}}", @@ -1827,11 +1828,15 @@ "mentions": "Mentions", "admin": "Admin", "replies": "Replies", + "replied": "Replied", "identity-verification": "Identity verification", - "identity-verification-request": "Identity verification request", - "identity-verification-request-sent": "asked you", + "identity-verification-request": "Identity verification", + "identity-verification-request-sent": "asks", + "type-something": "Type something", + "your-answer": "Your answer", "membership": "Membership", "jump-to": "Jump to", + "untrustworthy": "Untrustworthy", "blank-messages-text": "Your messages will be here", "groups": "Groups", "shell-placeholder-title": "Your apps will run here",