Implement identity verification flow (#14365)

This commit is contained in:
Icaro Motta 2022-11-16 16:46:04 -03:00 committed by GitHub
parent 509ffc2a01
commit 0f7ccce3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 604 additions and 332 deletions

View File

@ -1,10 +1,50 @@
(ns quo2.components.notifications.activity-logs (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.buttons.button :as button]
[quo2.components.icon :as icon] [quo2.components.icon :as icon]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
[quo2.components.tags.status-tags :as status-tags] [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 (defn- activity-icon
[icon] [icon]
@ -14,7 +54,6 @@
:margin-top 10 :margin-top 10
:border-width 1 :border-width 1
:border-color colors/white-opa-5 :border-color colors/white-opa-5
:flex-direction :column
:align-items :center :align-items :center
:justify-content :center} :justify-content :center}
[icon/icon icon {:color colors/white}]]) [icon/icon icon {:color colors/white}]])
@ -29,17 +68,17 @@
:border-radius 4}]) :border-radius 4}])
(defn- activity-context (defn- activity-context
[context] [context replying?]
(let [margin-top 4] (let [first-line-offset (if replying? 4 -2)
(into [rn/view {:flex 1 gap-between-lines 4]
:flex-direction :row (into [rn/view {:flex-direction :row
:align-items :center :align-items :center
:flex-wrap :wrap :flex-wrap :wrap
:margin-top (+ 4 (- margin-top))}] :margin-top first-line-offset}]
(map-indexed (fn [index detail] (map-indexed (fn [index detail]
^{:key index} ^{:key index}
[rn/view {:margin-right 4 [rn/view {:margin-right 4
:margin-top margin-top} :margin-top gap-between-lines}
(if (string? detail) (if (string? detail)
[text/text {:size :paragraph-2} [text/text {:size :paragraph-2}
detail] detail]
@ -52,12 +91,11 @@
:margin-top 12 :margin-top 12
:padding-horizontal 12 :padding-horizontal 12
:padding-vertical 8 :padding-vertical 8
:background-color colors/white-opa-5 :background-color colors/white-opa-5}
:flex 1
:flex-direction :column}
(when title (when title
[text/text {:size :paragraph-2 [text/text {:size :paragraph-2
:style {:color colors/white-opa-40}} :style {:color colors/white-opa-40
:margin-bottom 2}}
title]) title])
(if (string? body) (if (string? body)
[text/text {:style {:color colors/white} [text/text {:style {:color colors/white}
@ -66,25 +104,24 @@
body)]) body)])
(defn- activity-buttons (defn- activity-buttons
[button-1 button-2] [button-1 button-2 replying? reply-input]
(let [size 24 (let [size (if replying? 40 24)
common-style {:padding-top 3 common-style (when replying?
:padding-right 8 {:padding-vertical 9
:padding-bottom 4 :flex-grow 1
:padding-left 8}] :flex-basis 0})]
[rn/view {:margin-top 12 [rn/view {:margin-top 12
:flex 1
:flex-direction :row :flex-direction :row
:align-items :flex-start} :align-items :flex-start}
(when button-1 (when button-1
[button/button (-> button-1 [button/button (-> button-1
(assoc :size size) (assoc :size size)
(assoc-in [:style :margin-right] 8) (update :style merge common-style {:margin-right 8}))
(update :style merge common-style))
(:label button-1)]) (:label button-1)])
(when button-2 (when button-2
[button/button (-> button-2 [button/button (-> button-2
(assoc :size size) (assoc :size size)
(assoc :disabled (and replying? (not (valid-reply? @reply-input))))
(update :style merge common-style)) (update :style merge common-style))
(:label button-2)])])) (:label button-2)])]))
@ -98,10 +135,10 @@
:status status}]]) :status status}]])
(defn- activity-title (defn- activity-title
[title] [title replying?]
[text/text {:weight :semi-bold [text/text {:weight :semi-bold
:style {:color colors/white} :style {:color colors/white}
:size :paragraph-1} :size (if replying? :heading-2 :paragraph-1)}
title]) title])
(defn- activity-timestamp (defn- activity-timestamp
@ -112,46 +149,54 @@
:color colors/neutral-40}} :color colors/neutral-40}}
timestamp]]) 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 (defn activity-log
[{:keys [button-1 [{:keys [icon
button-2
icon
message message
status
context context
timestamp timestamp
title title
unread?]}] replying?
unread?]
:as props}]
[rn/view {:flex-direction :row [rn/view {:flex-direction :row
:flex 1 :align-items :flex-start
:border-radius 16 :border-radius 16
:padding-top 8 :padding-top 8
:padding-horizontal 12 :padding-horizontal (if replying? 20 12)
:padding-bottom 12 :padding-bottom 12
:background-color (when unread? :background-color (when (and unread? (not replying?))
colors/primary-50-opa-10)} colors/primary-50-opa-10)}
[activity-icon icon] (when-not replying?
[rn/view {:flex-direction :column [activity-icon icon])
:padding-left 8 [rn/view {:padding-left (when-not replying? 8)
:flex 1} :flex-grow 1}
[rn/view {:flex 1 [rn/view {:flex-grow 1
:align-items :center :align-items :center
:flex-direction :row} :flex-direction :row}
[rn/view {:flex 1 [rn/view {:flex 1
:align-items :center :align-items :center
:flex-direction :row} :flex-direction :row}
[rn/view {:flex-shrink 1} [rn/view {:flex-shrink 1}
[activity-title title]] [activity-title title replying?]]
[activity-timestamp timestamp]] (when-not replying?
(when unread? [activity-timestamp timestamp])]
(when (and unread? (not replying?))
[activity-unread-dot])] [activity-unread-dot])]
(when context (when context
[activity-context context]) [activity-context context replying?])
(when message (when message
[activity-message message]) [activity-message message])
(cond [footer props]]])
(some? status)
[activity-status status]
(or button-1 button-2)
[activity-buttons button-1 button-2])]])

View File

@ -32,6 +32,7 @@
quo2.components.notifications.info-count quo2.components.notifications.info-count
quo2.components.notifications.notification-dot quo2.components.notifications.notification-dot
quo2.components.tags.tags quo2.components.tags.tags
quo2.components.tags.context-tags
quo2.components.tabs.tabs quo2.components.tabs.tabs
quo2.components.tabs.account-selector quo2.components.tabs.account-selector
quo2.components.navigation.top-nav quo2.components.navigation.top-nav
@ -52,6 +53,7 @@
(def system-message quo2.components.messages.system-message/system-message) (def system-message quo2.components.messages.system-message/system-message)
(def reaction quo2.components.reactions.reaction/reaction) (def reaction quo2.components.reactions.reaction/reaction)
(def tags quo2.components.tags.tags/tags) (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 tabs quo2.components.tabs.tabs/tabs)
(def scrollable-tabs quo2.components.tabs.tabs/scrollable-tabs) (def scrollable-tabs quo2.components.tabs.tabs/scrollable-tabs)
(def account-selector quo2.components.tabs.account-selector/account-selector) (def account-selector quo2.components.tabs.account-selector/account-selector)
@ -88,4 +90,4 @@
;;;; NOTIFICATIONS ;;;; NOTIFICATIONS
(def activity-log quo2.components.notifications.activity-logs/activity-log) (def activity-log quo2.components.notifications.activity-logs/activity-log)
(def info-count quo2.components.notifications.info-count/info-count) (def info-count quo2.components.notifications.info-count/info-count)
(def notification-dot quo2.components.notifications.notification-dot/notification-dot) (def notification-dot quo2.components.notifications.notification-dot/notification-dot)

View File

@ -6,6 +6,14 @@
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[taoensso.timbre :as log])) [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 ;;;; Notification reconciliation
(defn- update-notifications (defn- update-notifications
@ -45,38 +53,69 @@
{:db (update-in db [:activity-center :notifications] {:db (update-in db [:activity-center :notifications]
update-notifications new-notifications)})) update-notifications new-notifications)}))
;;;; Contact verification (fx/defn notifications-reconcile-from-response
{:events [:activity-center/reconcile-notifications-from-response]}
(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]}
[cofx response] [cofx response]
(->> response (->> response
:activityCenterNotifications :activityCenterNotifications
(map data-store.activities/<-rpc) (map data-store.activities/<-rpc)
(notifications-reconcile cofx))) (notifications-reconcile cofx)))
(fx/defn contact-verification-decline-error ;;;; Contact verification
{:events [:activity-center.contact-verification/decline-error]}
[_ contact-verification-id error] (fx/defn contact-verification-decline
(log/warn "Failed to decline contact verification" {:events [:activity-center.contact-verification/decline]}
{:contact-verification-id contact-verification-id [_ notification-id]
:error error}) {::json-rpc/call [{:method "wakuext_declineContactVerificationRequest"
nil) :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 ;;;; Notifications fetching and pagination
(def defaults (def defaults
{:filter-status :unread {:filter-status :unread
:filter-type types/no-type :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 (def start-or-end-cursor
"") "")
@ -92,9 +131,9 @@
(defn status [filter-status] (defn status [filter-status]
(case filter-status (case filter-status
:read status-read :read status-read
:unread status-unread :unread status-unread
:all status-all :all status-all
99)) 99))
(fx/defn notifications-fetch (fx/defn notifications-fetch

View File

@ -3,6 +3,7 @@
[day8.re-frame.test :as rf-test] [day8.re-frame.test :as rf-test]
[re-frame.core :as rf] [re-frame.core :as rf]
[status-im.activity-center.notification-types :as types] [status-im.activity-center.notification-types :as types]
[status-im.constants :as constants]
[status-im.ethereum.json-rpc :as json-rpc] [status-im.ethereum.json-rpc :as json-rpc]
status-im.events status-im.events
[status-im.test-helpers :as h] [status-im.test-helpers :as h]
@ -14,87 +15,140 @@
;;;; Contact verification ;;;; 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 (deftest contact-verification-decline-test
(with-redefs [config/new-activity-center-enabled? true] (with-redefs [config/new-activity-center-enabled? true]
(testing "successfully declines and reconciles returned notification" (testing "declines notification and reconciles"
(rf-test/run-test-sync (test-contact-verification-event
(setup) {:event [:activity-center.contact-verification/decline notification-id]
(let [spy-queue (atom []) :expected-rpc-call {:method "wakuext_declineContactVerificationRequest"
contact-verification-id 24 :params [notification-id]}}))
expected-notification {:accepted false (testing "logs on failure"
:author "0x04d03f" (test-log-on-failure
:chat-id "0x04d03f" {:notification-id notification-id
:contact-verification-status 3 :event [:activity-center.contact-verification/decline notification-id]
:dismissed false :action :contact-verification/decline}))))
: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}]}))
(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" (deftest contact-verification-mark-as-untrustworthy-test
:params [contact-verification-id]} (with-redefs [config/new-activity-center-enabled? true]
(-> @spy-queue (testing "marks notification as untrustworthy and reconciles"
(get-in [0 :args 0]) (test-contact-verification-event
(select-keys [:method :params])))) {:event [:activity-center.contact-verification/mark-as-untrustworthy notification-id]
:expected-rpc-call {:method "wakuext_verifiedUntrustworthy"
(is (= {types/no-type :params [{:id notification-id}]}}))
{:read {:data [expected-notification]} (testing "logs on failure"
:unread {:data []}} (test-log-on-failure
types/contact-verification {:notification-id notification-id
{:read {:data [expected-notification]} :event [:activity-center.contact-verification/mark-as-untrustworthy notification-id]
:unread {:data []}}} :action :contact-verification/mark-as-untrustworthy}))))
(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))))))))))
;;;; Notification reconciliation ;;;; Notification reconciliation

View File

@ -6,7 +6,7 @@
(def ^:const mention 3) (def ^:const mention 3)
(def ^:const reply 4) (def ^:const reply 4)
(def ^:const contact-request 5) (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. ;; TODO: Remove this constant once the old Notification Center code is removed.
;; Its value clashes with the new constant `-contact-verification` ;; Its value clashes with the new constant `-contact-verification`

View File

@ -21,12 +21,13 @@
(def ^:const contact-request-state-received 3) (def ^:const contact-request-state-received 3)
(def ^:const contact-request-state-dismissed 4) (def ^:const contact-request-state-dismissed 4)
(def ^:const contact-verification-state-unknown 0) (def ^:const contact-verification-status-unknown 0)
(def ^:const contact-verification-state-pending 1) (def ^:const contact-verification-status-pending 1)
(def ^:const contact-verification-state-accepted 2) (def ^:const contact-verification-status-accepted 2)
(def ^:const contact-verification-state-declined 3) (def ^:const contact-verification-status-declined 3)
(def ^:const contact-verification-state-cancelled 4) (def ^:const contact-verification-status-cancelled 4)
(def ^:const contact-verification-state-trusted 5) (def ^:const contact-verification-status-trusted 5)
(def ^:const contact-verification-status-untrustworthy 6)
(def ^:const emoji-reaction-love 1) (def ^:const emoji-reaction-love 1)
(def ^:const emoji-reaction-thumbs-up 2) (def ^:const emoji-reaction-thumbs-up 2)

View File

@ -13,7 +13,7 @@
(def raw-notification (def raw-notification
{:chatId chat-id {:chatId chat-id
:contactVerificationStatus constants/contact-verification-state-pending :contactVerificationStatus constants/contact-verification-status-pending
:lastMessage {} :lastMessage {}
:name chat-name :name chat-name
:replyMessage {}}) :replyMessage {}})
@ -23,7 +23,7 @@
(testing "renames keys" (testing "renames keys"
(is (= {:name chat-name (is (= {:name chat-name
:chat-id chat-id :chat-id chat-id
:contact-verification-status constants/contact-verification-state-pending} :contact-verification-status constants/contact-verification-status-pending}
(-> raw-notification (-> raw-notification
store/<-rpc store/<-rpc
(dissoc :last-message :message :reply-message))))) (dissoc :last-message :message :reply-message)))))

View File

@ -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})

View File

@ -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))])))

View File

@ -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})

View File

@ -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}])})})))])))))

View File

@ -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})

View File

@ -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})

View File

@ -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])))

View File

@ -1,154 +1,32 @@
(ns status-im.ui.screens.activity-center.views (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 :as react]
[quo.react-native :as rn] [quo.react-native :as rn]
[quo2.components.buttons.button :as button] [quo2.core :as quo2]
[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.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[status-im.constants :as constants]
[status-im.activity-center.notification-types :as types] [status-im.activity-center.notification-types :as types]
[status-im.i18n.i18n :as i18n] [status-im.i18n.i18n :as i18n]
[status-im.multiaccounts.core :as multiaccounts] [status-im.ui.screens.activity-center.notification.contact-request.view :as contact-request]
[status-im.utils.datetime :as datetime] [status-im.ui.screens.activity-center.notification.contact-verification.view :as contact-verification]
[goog.string :as gstring] [status-im.ui.screens.activity-center.style :as style]
[status-im.utils.handlers :refer [<sub >evt]] [utils.re-frame :as rf]))
[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 (<sub [:contacts/contact-by-identity (:author notification)])
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 []
(>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 (<sub [:contacts/contact-by-identity (:author notification)])]
[activity-logs/activity-log
(merge {:title (i18n/label :t/identity-verification-request)
:icon :i/friend
: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}}
(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]])
(defn filter-selector-read-toggle (defn filter-selector-read-toggle
[] []
(let [unread-filter-enabled? (<sub [:activity-center/filter-status-unread-enabled?])] (let [unread-filter-enabled? (rf/sub [:activity-center/filter-status-unread-enabled?])]
;; TODO: Replace the button by a Filter Selector component once available for use. ;; TODO(@ilmotta): Replace the button by a Filter Selector.
[button/button {:icon true ;; https://github.com/status-im/status-mobile/issues/14355
:type (if unread-filter-enabled? :primary :blur-bg-outline) [quo2/button {:icon true
:size 32 :type (if unread-filter-enabled? :primary :blur-bg-outline)
:override-theme :dark :size 32
:on-press #(>evt [:activity-center.notifications/fetch-first-page :override-theme :dark
{:filter-status (if unread-filter-enabled? :on-press #(rf/dispatch [:activity-center.notifications/fetch-first-page
:all {:filter-status (if unread-filter-enabled?
:unread)}])} :all
:unread)}])}
:i/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 ;; shouldn't even care about translations at this point. A placeholder box is
;; used instead of an image. ;; used instead of an image.
(defn empty-tab (defn empty-tab
@ -161,24 +39,24 @@
:height 120 :height 120
:margin-bottom 20 :margin-bottom 20
:width 120}}] :width 120}}]
[text/text {:size :paragraph-1 [quo2/text {:size :paragraph-1
:style {:padding-bottom 2} :style {:padding-bottom 2}
:weight :semi-bold} :weight :semi-bold}
"No notifications"] "No notifications"]
[text/text {:size :paragraph-2} [quo2/text {:size :paragraph-2}
"Your notifications will be here"]]) "Your notifications will be here"]])
(defn tabs (defn tabs
[] []
(let [filter-type (<sub [:activity-center/filter-type])] (let [filter-type (rf/sub [:activity-center/filter-type])]
[tabs/scrollable-tabs {:size 32 [quo2/scrollable-tabs {:size 32
:blur? true :blur? true
:override-theme :dark :override-theme :dark
:style {:padding-left 20} :style style/tabs
:fade-end-percentage 0.79 :fade-end-percentage 0.79
:scroll-on-press? true :scroll-on-press? true
:fade-end? true :fade-end? true
:on-change #(>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 :default-active filter-type
:data [{:id types/no-type :data [{:id types/no-type
:label (i18n/label :t/all)} :label (i18n/label :t/all)}
@ -203,19 +81,16 @@
[] []
(let [screen-padding 20] (let [screen-padding 20]
[rn/view [rn/view
[button/button {:icon true [quo2/button {:icon true
:type :blur-bg :type :blur-bg
:size 32 :size 32
:override-theme :dark :override-theme :dark
:style {:margin-bottom 12 :style style/header-button
:margin-left screen-padding} :on-press #(rf/dispatch [:hide-popover])}
:on-press #(>evt [:hide-popover])}
:i/close] :i/close]
[text/text {:size :heading-1 [quo2/text {:size :heading-1
:weight :semi-bold :weight :semi-bold
:style {:padding-horizontal screen-padding :style style/header-heading}
:padding-vertical 12
:color colors/white}}
(i18n/label :t/notifications)] (i18n/label :t/notifications)]
[rn/view {:flex-direction :row [rn/view {:flex-direction :row
:padding-vertical 12} :padding-vertical 12}
@ -227,22 +102,31 @@
:padding-right screen-padding} :padding-right screen-padding}
[filter-selector-read-toggle]]]])) [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 (defn activity-center
[] []
[:f> [:f>
(fn [] (fn []
(let [notifications (<sub [:activity-center/filtered-notifications]) (let [notifications (rf/sub [:activity-center/filtered-notifications])
window-width (<sub [:dimensions/window-width]) window-width (rf/sub [:dimensions/window-width])
{:keys [top bottom]} (safe-area/use-safe-area)] {:keys [top bottom]} (safe-area/use-safe-area)]
(react/effect! #(>evt [:activity-center.notifications/fetch-first-page])) (react/effect! #(rf/dispatch [:activity-center.notifications/fetch-first-page]))
[rn/view {:style {:flex 1 [rn/view {:style (style/screen-container window-width top bottom)}
:width window-width
:padding-top (if (pos? top) (+ top 12) 12)
:padding-bottom bottom}}
[header] [header]
[rn/flat-list {:content-container-style {:flex-grow 1} [rn/flat-list {:content-container-style style/notifications-container
:data notifications :data notifications
:empty-component [empty-tab] :empty-component [empty-tab]
:key-fn :id :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}]]))]) :render-fn render-notification}]]))])

View File

@ -1,12 +1,13 @@
(ns status-im.ui.screens.bottom-sheets.views (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] [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.home.sheet.views :as home.sheet]
[status-im.ui.screens.keycard.views :as keycard] [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.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]))
[status-im.ui.screens.multiaccounts.recover.views :as recover.views]
[quo.core :as quo]))
(defn bottom-sheet [] (defn bottom-sheet []
(let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet]) (let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet])
@ -27,6 +28,9 @@
(= view :add-new) (= view :add-new)
(merge home.sheet/add-new) (merge home.sheet/add-new)
(= view :activity-center.contact-verification/reply)
(merge contact-verification.sheet/reply)
(= view :keycard.login/more) (= view :keycard.login/more)
(merge keycard/more-sheet) (merge keycard/more-sheet)
@ -40,4 +44,4 @@
(merge key-storage/migrate-account-password))] (merge key-storage/migrate-account-password))]
[quo/bottom-sheet opts [quo/bottom-sheet opts
(when content (when content
[content (when options options)])])) [content (when options options)])]))

View File

@ -11,6 +11,9 @@
(def descriptor [{:label "Unread?" (def descriptor [{:label "Unread?"
:key :unread? :key :unread?
:type :boolean} :type :boolean}
{:label "Replying?"
:key :replying?
:type :boolean}
{:label "Icon" {:label "Icon"
:key :icon :key :icon
:type :select :type :select
@ -138,6 +141,10 @@
(= (:message @state) :with-mention) (= (:message @state) :with-mention)
(assoc :message message-with-mention) (assoc :message message-with-mention)
(some? (:status @state))
(update :status (fn [status]
{:label (name status) :type status}))
(= (:message @state) :with-title) (= (:message @state) :with-title)
(assoc :message message-with-title) (assoc :message message-with-title)

View File

@ -3,7 +3,7 @@
"_comment": "Instead use: scripts/update-status-go.sh <rev>", "_comment": "Instead use: scripts/update-status-go.sh <rev>",
"owner": "status-im", "owner": "status-im",
"repo": "status-go", "repo": "status-go",
"version": "v0.115.0", "version": "v0.115.1",
"commit-sha1": "2341dedfbabfb8e392aaa49ecb9bb7fab6a74b99", "commit-sha1": "2572321063182c0a0376da8a3462544e598c0f9d",
"src-sha256": "1x8r3rln2vna82kkvl42q11wh7zn2acr3crzj63pzcawdsr6l990" "src-sha256": "07d996c10z4acin5wk21qyh9j1k1sc75a7x5d5hffaxllha7m9rs"
} }

View File

@ -1138,6 +1138,7 @@
"send-logs-to": "Report a bug to {{email}}", "send-logs-to": "Report a bug to {{email}}",
"send-message": "Send message", "send-message": "Send message",
"send-request": "Send request", "send-request": "Send request",
"send-reply": "Send reply",
"send-request-amount": "Amount", "send-request-amount": "Amount",
"send-request-amount-max-decimals": "Max number of decimals is {{asset-decimals}}", "send-request-amount-max-decimals": "Max number of decimals is {{asset-decimals}}",
"send-request-unknown-token": "Unknown token - {{asset}}", "send-request-unknown-token": "Unknown token - {{asset}}",
@ -1827,11 +1828,15 @@
"mentions": "Mentions", "mentions": "Mentions",
"admin": "Admin", "admin": "Admin",
"replies": "Replies", "replies": "Replies",
"replied": "Replied",
"identity-verification": "Identity verification", "identity-verification": "Identity verification",
"identity-verification-request": "Identity verification request", "identity-verification-request": "Identity verification",
"identity-verification-request-sent": "asked you", "identity-verification-request-sent": "asks",
"type-something": "Type something",
"your-answer": "Your answer",
"membership": "Membership", "membership": "Membership",
"jump-to": "Jump to", "jump-to": "Jump to",
"untrustworthy": "Untrustworthy",
"blank-messages-text": "Your messages will be here", "blank-messages-text": "Your messages will be here",
"groups": "Groups", "groups": "Groups",
"shell-placeholder-title": "Your apps will run here", "shell-placeholder-title": "Your apps will run here",