Swipe gestures for Activity Center notifications with CTA (#15284)

Implements swipe actions for notifications with call to action (e.g. pending
contact requests, unverified identity verifications, etc).

Fixes https://github.com/status-im/status-mobile/issues/15118

According to the Design team, the goal is to deliver a consistent experience to
users, so whenever the user sees a notification with buttons, the same actions
can be taken via the swipe buttons.

Note: swipe buttons are using placeholder icons while the Design team works out
which ones to use

Additionally, a bunch of fixes:

- Fix: outgoing pending contact requests were not being removed from the UI when
  cancelled.
- Fix: Membership tab not showing unread indicator.
- Fix: dismissed membership notification not marked as read.
- Fix: dismissed membership notification was displaying decline/accept buttons.
  Regression came from changes in status-go related to soft deletion of
  notifications.
- Fix: incorrect check for the pending state of a contact request.
- Fixed lots of bugs for identity verification notifications, as it was
  completely broken. Unfortunately, somebody made lots of changes without
  actually testing the flows.
- Add basic error handling and log if accepting, declining or canceling contact
  requests fail.

The demo shows an identity verification with swipe actions to reply or decline.
[identity-verification-swipe-to-reply.webm](https://user-images.githubusercontent.com/46027/223565755-b2ca3f68-12e2-4e1e-9e52-edd52cfcc971.webm)

Out of scope: The old quo input is still in use in the identity verification
notification. This will eventually be solved by issue
https://github.com/status-im/status-mobile/issues/14364

### Steps to test

Notifications with one or more buttons (actions) are affected by this change,
because now the user can also swipe left/right to act on them.

- Membership notifications: private group chat. The following PR explains how to
  generate them https://github.com/status-im/status-mobile/pull/14785
- Contact requests, and community gated requests to join (Admin tab).
- Identity verifications. I believe the only way to test identity verification
  flows at the moment is to use the Desktop app, since initiating the challenge
  is not implemented in Mobile yet.
- Mentions and replies don't have new swipe buttons because they don't have call
  to action buttons throughout their lifecycle.

Steps to test identity verification flows:

#### Identity verification flow 1

- `A` and `B` are mutual contacts.
- `A` sends a verification request to `B`.
- `A` should not see any notification yet.
- `B` should receive an identity verification notification. `B` can either
  decline or reply.
- `B` declines and the status `Declined` is shown instead of buttons.
- `B` can now either swipe to toggle read/unread or swipe delete the
  notification.
- `A` should not receive any notification after `A` declined.

#### Identity verification flow 2

- `A` and `B` are mutual contacts.
- `A` sends a verification request to `B`.
- `A` should not see any notification yet.
- `B` should receive an identity verification notification. `B` can either
  decline or reply.
- `B` press `Reply` and a bottom sheet is displayed with a text input.
- `B` sends the reply/answer message and the status `Replied` is shown instead
  of buttons.
- `B` can now either swipe to toggle read/unread or swipe to delete the
  notification.
- `A` should receive a notification with the reply from `B`.
- `A` can either mark the answer as untrustworthy or accept it (trust it) via
  the normal buttons, as well as via the swipe left/right buttons.
- If `A` accepts the answer, then the status `Confirmed` is shown instead of
  buttons. On the other hand, if `A` marks as untrustworthy, then the status
  `Untrustworthy` is shown instead of buttons.
- `B` should receive no further notifications due to `A`s actions.
- `A` can now either swipe to toggle read/unread or swipe delete the
  notification.
This commit is contained in:
Icaro Motta 2023-03-14 12:34:13 -03:00 committed by GitHub
parent e98ce54830
commit 9473d3f40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 672 additions and 390 deletions

View File

@ -37,7 +37,7 @@
(when on-update-reply
(on-update-reply %)))
:auto-capitalize :none
:auto-focus true
:auto-focus false
:accessibility-label :identity-verification-reply-text-input
:placeholder (i18n/label :t/type-something)
:return-key-type :none
@ -129,7 +129,7 @@
(-> button
(assoc :size size)
(assoc :type subtype)
(assoc :disabled (and replying? (disable-when @reply-input)))
(assoc :disabled (and replying? disable-when (disable-when @reply-input)))
(update :style merge common-style {:margin-right 8}))
label]))
@ -142,7 +142,7 @@
:blur? blur?}])
(defn- footer
[_ _]
[_]
(let [reply-input (reagent/atom "")]
(fn [{:keys [replying? items] :as props}]
[:<>

View File

@ -55,6 +55,7 @@
[status-im.wallet.core :as wallet]
status-im.wallet.custom-tokens.core
status-im2.contexts.activity-center.events
status-im2.contexts.activity-center.notification.contact-requests.events
status-im2.contexts.shell.events
status-im.chat.models.gaps
[status-im2.navigation.events :as navigation]))

View File

@ -177,7 +177,7 @@
(reset! expanded? true))
(and @keyboard-was-shown? (not keyboard-shown))
(reset! expanded? false))))
[@show-bottom-sheet? @keyboard-was-shown?])
[@show-bottom-sheet? @keyboard-was-shown? keyboard-shown])
(react/effect! #(do
(when-not @gesture-running?
(cond

View File

@ -5,7 +5,6 @@
[status-im2.common.toasts.events :as toasts]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification-types :as types]
status-im2.contexts.activity-center.notification.contact-requests.events
[status-im2.contexts.chat.events :as chat.events]
[taoensso.timbre :as log]
[utils.collection :as collection]
@ -43,6 +42,12 @@
(log/warn (str "Failed to " action)
{:notification-id notification-id :error error}))
(defn get-notification
[db notification-id]
(->> (get-in db [:activity-center :notifications])
(filter #(= notification-id (:id %)))
first))
;;;; Notification reconciliation
(defn- update-notifications
@ -94,12 +99,6 @@
;;;; Status changes (read/dismissed/deleted)
(defn- get-notification
[db notification-id]
(->> (get-in db [:activity-center :notifications])
(filter #(= notification-id (:id %)))
first))
(rf/defn mark-as-read
{:events [:activity-center.notifications/mark-as-read]}
[{:keys [db]} notification-id]
@ -235,7 +234,7 @@
{:events [:activity-center.notifications/dismiss-success]}
[{:keys [db] :as cofx} notification-id]
(let [notification (get-notification db notification-id)]
(notifications-reconcile cofx [(assoc notification :dismissed true)])))
(notifications-reconcile cofx [(assoc notification :read true :dismissed true)])))
(rf/defn delete-notification
{:events [:activity-center.notifications/delete]}

View File

@ -137,15 +137,16 @@
:action :notification/accept})))
(deftest notification-dismissal-test
(testing "dismisses notification, but keep it in the app db"
(testing "dismisses & mark notification as read, and keep it in the app db"
(h/run-test-sync
(setup)
(let [notif-1 {:id "0x1" :type types/private-group-chat}
notif-2 {:id "0x2" :type types/admin}
dismissed-notif-1 (assoc notif-1 :dismissed true)]
dismissed-notif-1 (assoc notif-1 :dismissed true :read true)]
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2))
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
[notif-2 notif-1]])
(rf/dispatch [:test/assoc-in [:activity-center]
{:filter {:type types/no-type :status :all}
:notifications [notif-2 notif-1]}])
(rf/dispatch [:activity-center.notifications/dismiss (:id notif-1)])
@ -520,7 +521,7 @@
:type types/mention}]
(get-in (h/db) [:activity-center :notifications]))))))
(testing "resets loading flag after an error"
(testing "resets loading state after error"
(h/run-test-sync
(setup)
(let [spy-queue (atom [])]

View File

@ -2,33 +2,64 @@
(:require [quo2.core :as quo]
[quo2.foundations.colors :as colors]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification.common.style :as style]
[status-im2.contexts.activity-center.notification.common.style :as common-style]
[status-im2.contexts.activity-center.notification.common.view :as common]
[utils.datetime :as datetime]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn swipeable
[{:keys [height active-swipeable notification]} child]
(if (#{constants/activity-center-membership-status-accepted
(defn- swipe-button-accept
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-success-container style)
:icon :i/placeholder
:text (i18n/label :t/accept)}])
(defn- swipe-button-decline
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/decline)}])
(defn- swipeable
[{:keys [active-swipeable notification extra-fn]} child]
(let [{:keys [community-id id membership-status]} notification]
(cond
(#{constants/activity-center-membership-status-accepted
constants/activity-center-membership-status-declined}
(:membership-status notification))
membership-status)
[common/swipeable
{:left-button common/left-swipe-button
:left-on-press common/left-swipe-on-press
:right-button common/right-swipe-button
:right-on-press common/right-swipe-on-press
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn (fn [] {:height @height :notification notification})}
:extra-fn extra-fn}
child]
child))
(= membership-status constants/activity-center-membership-status-pending)
[common/swipeable
{:left-button swipe-button-accept
:left-on-press #(rf/dispatch [:communities.ui/accept-request-to-join-pressed community-id id])
:right-button swipe-button-decline
:right-on-press #(rf/dispatch [:communities.ui/decline-request-to-join-pressed community-id
id])
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
:else
child)))
(defn view
[{:keys [author community-id id membership-status read timestamp]}
set-swipeable-height]
(let [community (rf/sub [:communities/community community-id])
[{:keys [notification set-swipeable-height] :as props}]
(let [{:keys [author community-id id membership-status
read timestamp]} notification
community (rf/sub [:communities/community community-id])
community-name (:name community)
community-image (get-in community [:images :thumbnail :uri])]
[swipeable props
[quo/activity-log
{:title (i18n/label :t/join-request)
:icon :i/add-user
@ -41,8 +72,8 @@
{:size :small
:override-theme :dark
:color colors/primary-50
:style style/user-avatar-tag
:text-style style/user-avatar-tag-text}
:style common-style/user-avatar-tag
:text-style common-style/user-avatar-tag-text}
{:uri community-image} community-name]]
:items (case membership-status
constants/activity-center-membership-status-accepted
@ -66,7 +97,8 @@
:label (i18n/label :t/decline)
:accessibility-label :decline-join-request
:on-press (fn []
(rf/dispatch [:communities.ui/decline-request-to-join-pressed
(rf/dispatch
[:communities.ui/decline-request-to-join-pressed
community-id id]))}
{:type :button
:subtype :positive
@ -77,4 +109,4 @@
(rf/dispatch [:communities.ui/accept-request-to-join-pressed
community-id id]))}]
nil)}]))
nil)}]]))

View File

@ -30,23 +30,29 @@
:outputRange [0 swipe-action-width]
:extrapolate :clamp}))
(defn left-swipe-container
[style-props]
(merge {:background-color colors/primary-60
:align-items :center
(def swipe-base
{:align-items :center
:justify-content :center
:border-radius swipe-button-border-radius
:width swipe-action-width}
style-props))
:width swipe-action-width})
(defn right-swipe-container
[style-props]
(merge {:background-color colors/danger-60
:align-items :center
:justify-content :center
:border-radius swipe-button-border-radius
:width swipe-action-width}
style-props))
(defn swipe-success-container
[style]
(merge swipe-base
{:background-color colors/success-60}
style))
(defn swipe-danger-container
[style]
(merge swipe-base
{:background-color colors/danger-60}
style))
(defn swipe-primary-container
[style]
(merge swipe-base
{:background-color colors/primary-60}
style))
(def swipe-text
{:margin-top 5

View File

@ -54,39 +54,42 @@
(.close ^js @active-swipeable))
(reset! active-swipeable @swipeable)))
(defn left-swipe-button
[{:keys [style]} {:keys [notification]}]
(defn swipe-button-container
[{:keys [style icon text]} _]
[rn/animated-view
{:accessibility-label :notification-left-swipe
:style (style/left-swipe-container style)}
{:accessibility-label :notification-swipe
:style style}
[rn/view {:style style/swipe-text-wrapper}
[quo/icon
(if (:read notification)
:i/notifications
:i/check)
[quo/icon icon
{:color colors/white}]
[quo/text {:style style/swipe-text}
(if (:read notification)
text]]])
(defn swipe-button-read-or-unread
[{:keys [style]} {:keys [notification]}]
[swipe-button-container
{:style (style/swipe-primary-container style)
:icon (if (:read notification)
:i/notifications
:i/check)
:text (if (:read notification)
(i18n/label :t/unread)
(i18n/label :t/read))]]])
(i18n/label :t/read))}])
(defn right-swipe-button
(defn swipe-button-delete
[{:keys [style]}]
[rn/animated-view
{:accessibility-label :notification-right-swipe
:style (style/right-swipe-container style)}
[rn/view {:style style/swipe-text-wrapper}
[quo/icon :i/delete {:color colors/white}]
[quo/text {:style style/swipe-text}
(i18n/label :t/delete)]]])
[swipe-button-container
{:style (style/swipe-danger-container style)
:icon :i/delete
:text (i18n/label :t/delete)}])
(defn left-swipe-on-press
(defn swipe-on-press-toggle-read
[{:keys [notification]}]
(if (:read notification)
(rf/dispatch [:activity-center.notifications/mark-as-unread (:id notification)])
(rf/dispatch [:activity-center.notifications/mark-as-read (:id notification)])))
(defn right-swipe-on-press
(defn swipe-on-press-delete
[{:keys [notification]}]
(rf/dispatch [:activity-center.notifications/delete (:id notification)]))
@ -102,14 +105,14 @@
& children]
(into
[gesture/swipeable
(merge
{:ref #(reset! swipeable-ref %)
:accessibility-label :notification-swipeable
:friction 2
:on-swipeable-will-open (close-active-swipeable active-swipeable swipeable-ref)}
(when left-button
{:overshoot-left false
:left-threshold style/swipe-action-width
:right-threshold style/swipe-action-width
:overshoot-left false
:overshoot-right false
:on-swipeable-will-open (close-active-swipeable active-swipeable swipeable-ref)
:render-left-actions (render-swipe-action
{:active-swipeable active-swipeable
:extra-fn extra-fn
@ -118,7 +121,10 @@
style/left-swipe-translate-x-interpolation-js
:on-press left-on-press
:swipe-button left-button
:swipeable-ref swipeable-ref})
:swipeable-ref swipeable-ref})})
(when right-button
{:overshoot-right false
:right-threshold style/swipe-action-width
:render-right-actions (render-swipe-action
{:active-swipeable active-swipeable
:extra-fn extra-fn
@ -127,5 +133,5 @@
style/right-swipe-translate-x-interpolation-js
:on-press right-on-press
:swipe-button right-button
:swipeable-ref swipeable-ref})}]
:swipeable-ref swipeable-ref})}))]
children))))

View File

@ -1,26 +1,69 @@
(ns status-im2.contexts.activity-center.notification.contact-requests.events
(:require [utils.re-frame :as rf]))
(:require [status-im2.contexts.activity-center.events :as ac-events]
[taoensso.timbre :as log]
[utils.re-frame :as rf]))
(rf/defn accept-contact-request
{:events [:activity-center.contact-requests/accept-request]}
[{:keys [db]} id]
{:json-rpc/call [{:method "wakuext_acceptContactRequest"
:params [{:id id}]
{:events [:activity-center.contact-requests/accept]}
[_ contact-id]
{:json-rpc/call
[{:method "wakuext_acceptContactRequest"
:params [{:id contact-id}]
:js-response true
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])}]})
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])
:on-error #(rf/dispatch [:activity-center.contact-requests/accept-error contact-id %])}]})
(rf/defn accept-contact-request-error
{:events [:activity-center.contact-requests/accept-error]}
[_ contact-id error]
(log/error "Failed to accept contact-request"
{:error error
:event :activity-center.contact-requests/accept
:contact-id contact-id})
nil)
(rf/defn decline-contact-request
{:events [:activity-center.contact-requests/decline-request]}
[{:keys [db]} id]
{:json-rpc/call [{:method "wakuext_declineContactRequest"
:params [{:id id}]
{:events [:activity-center.contact-requests/decline]}
[_ contact-id]
{:json-rpc/call
[{:method "wakuext_declineContactRequest"
:params [{:id contact-id}]
:js-response true
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])}]})
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])
:on-error #(rf/dispatch [:activity-center.contact-requests/decline-error contact-id %])}]})
(rf/defn decline-contact-request-error
{:events [:activity-center.contact-requests/decline-error]}
[_ contact-id error]
(log/error "Failed to decline contact-request"
{:error error
:event :activity-center.contact-requests/decline
:contact-id contact-id})
nil)
(rf/defn cancel-outgoing-contact-request
{:events [:activity-center.contact-requests/cancel-outgoing-request]}
[{:keys [db]} id]
{:json-rpc/call [{:method "wakuext_cancelOutgoingContactRequest"
:params [{:id id}]
:js-response true
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])}]})
{:events [:activity-center.contact-requests/cancel-outgoing]}
[{:keys [db]} {:keys [contact-id notification-id]}]
(when-let [notification (ac-events/get-notification db notification-id)]
{:json-rpc/call
[{:method "wakuext_cancelOutgoingContactRequest"
:params [{:id contact-id}]
:on-success #(rf/dispatch [:activity-center.contact-requests/cancel-outgoing-success
notification])
:on-error #(rf/dispatch [:activity-center.contact-requests/cancel-outgoing-error contact-id
%])}]}))
(rf/defn cancel-outgoing-contact-request-success
{:events [:activity-center.contact-requests/cancel-outgoing-success]}
[_ notification]
{:dispatch [:activity-center.notifications/reconcile
[(assoc notification :deleted true)]]})
(rf/defn cancel-outgoing-contact-request-error
{:events [:activity-center.contact-requests/cancel-outgoing-error]}
[_ contact-id error]
(log/error "Failed to cancel outgoing contact-request"
{:error error
:event :activity-center.contact-requests/cancel-outgoing
:contact-id contact-id})
nil)

View File

@ -2,31 +2,81 @@
(:require [quo2.core :as quo]
[react-native.gesture :as gesture]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.notification.common.style :as common-style]
[status-im2.contexts.activity-center.notification.common.view :as common]
[utils.datetime :as datetime]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn swipeable
[{:keys [height active-swipeable notification]} child]
(let [message (or (:message notification) (:last-message notification))]
(if (#{constants/contact-request-message-state-accepted
(defn- swipe-button-accept
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-success-container style)
:icon :i/placeholder
:text (i18n/label :t/accept)}])
(defn- swipe-button-decline
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/decline)}])
(defn- swipe-button-cancel-pending
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/cancel)}])
(defn- swipeable
[{:keys [active-swipeable extra-fn notification]} child]
(let [{:keys [id author message last-message]} notification
{:keys [contact-request-state]} (or (:message notification)
(:last-message notification))
{:keys [public-key]} (rf/sub [:multiaccount/contact])
message (or message last-message)]
(cond
(#{constants/contact-request-message-state-accepted
constants/contact-request-message-state-declined}
(:contact-request-state message))
contact-request-state)
[common/swipeable
{:left-button common/left-swipe-button
:left-on-press common/left-swipe-on-press
:right-button common/right-swipe-button
:right-on-press common/right-swipe-on-press
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn (fn [] {:height @height :notification notification})}
:extra-fn extra-fn}
child]
(= contact-request-state constants/contact-request-message-state-pending)
(if (= public-key author)
[common/swipeable
{:right-button swipe-button-cancel-pending
:right-on-press (fn []
(rf/dispatch
[:activity-center.contact-requests/cancel-outgoing
{:contact-id (:from message)
:notification-id id}]))
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
[common/swipeable
{:left-button swipe-button-accept
:left-on-press #(rf/dispatch [:activity-center.contact-requests/accept id])
:right-button swipe-button-decline
:right-on-press #(rf/dispatch [:activity-center.contact-requests/decline id])
:active-swipeable active-swipeable
:extra-fn extra-fn}
child])
:else
child)))
(defn outgoing-contact-request-view
[{:keys [id chat-id message last-message] :as notification}
set-swipeable-height]
(let [{:keys [contact-request-state] :as message} (or message last-message)]
(defn- outgoing-contact-request-view
[{:keys [notification set-swipeable-height]}]
(let [{:keys [id chat-id message last-message]} notification
{:keys [contact-request-state] :as message} (or message last-message)]
(if (= contact-request-state constants/contact-request-message-state-accepted)
[quo/activity-log
{:title (i18n/label :t/contact-request-was-accepted)
@ -48,7 +98,7 @@
[common/user-avatar-tag chat-id]]
:message {:body (get-in message [:content :text])}
:items (case contact-request-state
constants/contact-request-state-mutual
constants/contact-request-message-state-pending
[{:type :button
:subtype :danger
:key :button-cancel
@ -56,10 +106,9 @@
:accessibility-label :cancel-contact-request
:on-press (fn []
(rf/dispatch
[:activity-center.contact-requests/cancel-outgoing-request
(:from message)])
(rf/dispatch [:activity-center.notifications/mark-as-read
id]))}
[:activity-center.contact-requests/cancel-outgoing
{:contact-id (:from message)
:notification-id id}]))}
{:type :status
:subtype :pending
:key :status-pending
@ -75,10 +124,10 @@
nil)}])))
(defn incoming-contact-request-view
[{:keys [id author message last-message] :as notification}
set-swipeable-height]
(let [message (or message last-message)]
(defn- incoming-contact-request-view
[{:keys [notification set-swipeable-height]}]
(let [{:keys [id author message last-message]} notification
message (or message last-message)]
[quo/activity-log
{:title (i18n/label :t/contact-request)
:on-layout set-swipeable-height
@ -110,36 +159,31 @@
:key :button-decline
:label (i18n/label :t/decline)
:accessibility-label :decline-contact-request
:on-press (fn []
(rf/dispatch [:activity-center.contact-requests/decline-request id])
(rf/dispatch [:activity-center.notifications/mark-as-read
id]))}
:on-press #(rf/dispatch [:activity-center.contact-requests/decline id])}
{:type :button
:subtype :positive
:key :button-accept
:label (i18n/label :t/accept)
:accessibility-label :accept-contact-request
:on-press (fn []
(rf/dispatch [:activity-center.contact-requests/accept-request id])
(rf/dispatch [:activity-center.notifications/mark-as-read
id]))}]
:on-press #(rf/dispatch [:activity-center.contact-requests/accept id])}]
nil)}]))
(defn view
[{:keys [author message last-message] :as notification}
set-swipeable-height]
(let [{:keys [public-key]} (rf/sub [:multiaccount/contact])
[{:keys [notification] :as props}]
(let [{:keys [author message last-message]} notification
{:keys [public-key]} (rf/sub [:multiaccount/contact])
{:keys [contact-request-state]} (or message last-message)]
[swipeable props
(cond
(= public-key author)
[outgoing-contact-request-view notification set-swipeable-height]
[outgoing-contact-request-view props]
(= contact-request-state constants/contact-request-message-state-accepted)
[gesture/touchable-without-feedback
{:on-press (fn []
(rf/dispatch [:hide-popover])
(rf/dispatch [:chat.ui/start-chat author]))}
[incoming-contact-request-view notification set-swipeable-height]]
[incoming-contact-request-view props]]
:else
[incoming-contact-request-view notification set-swipeable-height])))
[incoming-contact-request-view props])]))

View File

@ -1,16 +1,40 @@
(ns status-im2.contexts.activity-center.notification.contact-verification.view
(:require [clojure.string :as string]
[utils.i18n :as i18n]
[quo2.core :as quo]
[status-im2.constants :as constants]
[utils.datetime :as datetime]
[status-im2.contexts.activity-center.notification.common.style :as common-style]
[status-im2.contexts.activity-center.notification.common.view :as common]
[utils.datetime :as datetime]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn- hide-bottom-sheet-and-dispatch
[event]
(rf/dispatch [:bottom-sheet/hide])
(rf/dispatch event))
(defn- swipe-button-decline
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/decline)}])
(defn- swipe-button-reply
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-primary-container style)
:icon :i/placeholder
:text (i18n/label :t/message-reply)}])
(defn- swipe-button-untrustworthy
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/untrustworthy)}])
(defn- swipe-button-trust
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-success-container style)
:icon :i/placeholder
:text (i18n/label :t/accept)}])
(defn- context-tags
[challenger? {:keys [author contact-verification-status]}]
@ -38,18 +62,6 @@
(= 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)})))
(def ^:private max-reply-length
280)
@ -57,18 +69,105 @@
[reply]
(<= (count reply) max-reply-length))
(def ^:private invalid-reply?
(comp not valid-reply?))
(declare view)
(defn- decline-challenge
[id]
(rf/dispatch [:bottom-sheet/hide])
(rf/dispatch [:activity-center.contact-verification/decline id])
(rf/dispatch [:activity-center.notifications/mark-as-read id]))
(defn- prepare-challenge-reply
[props]
(rf/dispatch [:bottom-sheet/show-sheet
{:content view
:override-theme :dark}
(assoc props :replying? true)]))
(defn- send-challenge-reply
[id reply]
(rf/dispatch [:bottom-sheet/hide])
(rf/dispatch [:activity-center.contact-verification/reply id reply])
(rf/dispatch [:activity-center.notifications/mark-as-read id]))
(defn- mark-challenge-untrustworthy
[id]
(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy id])
(rf/dispatch [:activity-center.notifications/mark-as-read id]))
(defn- mark-challenge-trusted
[id]
(rf/dispatch [:activity-center.contact-verification/mark-as-trusted id])
(rf/dispatch [:activity-center.notifications/mark-as-read id]))
(defn- swipeable
[{:keys [active-swipeable extra-fn notification replying?] :as props} child]
(let [{:keys [id message
contact-verification-status]} notification
challenger? (:outgoing message)]
(cond
replying?
child
(and (not challenger?)
(= contact-verification-status constants/contact-verification-status-pending))
[common/swipeable
{:left-button swipe-button-reply
:left-on-press #(prepare-challenge-reply props)
:right-button swipe-button-decline
:right-on-press #(decline-challenge id)
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
(and challenger?
(= contact-verification-status constants/contact-verification-status-accepted))
[common/swipeable
{:left-button swipe-button-trust
:left-on-press #(mark-challenge-trusted id)
:right-button swipe-button-untrustworthy
:right-on-press #(mark-challenge-untrustworthy id)
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
(#{constants/contact-verification-status-accepted
constants/contact-verification-status-declined
constants/contact-verification-status-trusted}
contact-verification-status)
[common/swipeable
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
:else
child)))
(defn view
[_ _]
[_]
(let [reply (atom "")]
(fn [{:keys [id message contact-verification-status] :as notification} {:keys [replying?]}]
(let [challenger? (:outgoing message)]
(fn [{:keys [notification set-swipeable-height replying?] :as props}]
(let [{:keys [id message
contact-verification-status]} notification
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?
(when-not
(and challenger?
(= contact-verification-status constants/contact-verification-status-declined))
[swipeable props
[quo/activity-log
(merge
(when-not replying?
{:on-layout set-swipeable-height})
{:title (i18n/label :t/identity-verification-request)
:icon :i/friend
:timestamp (datetime/timestamp->relative (:timestamp notification))
@ -79,62 +178,72 @@
:valid-reply? valid-reply?
:context (context-tags challenger? notification)
:message (activity-message challenger? notification)
:status (activity-status challenger? contact-verification-status)
:items
(if challenger?
(when (= contact-verification-status constants/contact-verification-status-accepted)
(cond-> []
(and challenger?
(= contact-verification-status constants/contact-verification-status-accepted))
(concat
[{:type :button
:subtype :danger
:key :button-mark-as-untrustworthy
:label (i18n/label :t/untrustworthy)
:accessibility-label :mark-contact-verification-as-untrustworthy
:on-press (fn []
(rf/dispatch
[:activity-center.contact-verification/mark-as-untrustworthy
id])
(rf/dispatch [:activity-center.notifications/mark-as-read
id]))}
:on-press #(mark-challenge-untrustworthy id)}
{:type :button
:subtype :positive
:key :button-accept
:label (i18n/label :t/accept)
:accessibility-label :mark-contact-verification-as-trusted
:on-press (fn []
(rf/dispatch
[:activity-center.contact-verification/mark-as-trusted id])
(rf/dispatch [:activity-center.notifications/mark-as-read
id]))}])
(when (= contact-verification-status constants/contact-verification-status-pending)
:on-press #(mark-challenge-trusted id)}])
(and challenger?
(= contact-verification-status constants/contact-verification-status-trusted))
(concat [{:type :status
:subtype :positive
:key :status-trusted
:label (i18n/label :t/status-confirmed)}])
(and challenger?
(= contact-verification-status constants/contact-verification-status-untrustworthy))
(concat [{:type :status
:subtype :negative
:key :status-untrustworthy
:label (i18n/label :t/untrustworthy)}])
(and (not challenger?)
(= contact-verification-status constants/contact-verification-status-accepted))
(concat [{:type :status
:subtype :positive
:key :status-accepted
:label (i18n/label :t/replied)}])
(and (not challenger?)
(= contact-verification-status constants/contact-verification-status-declined))
(concat [{:type :status
:subtype :negative
:key :status-declined
:label (i18n/label :t/declined)}])
(and (not challenger?)
(= contact-verification-status constants/contact-verification-status-pending))
(concat
[{:type :button
:subtype :danger
:key :button-decline
:label (i18n/label :t/decline)
:accessibility-label :decline-contact-verification
:on-press (fn []
(hide-bottom-sheet-and-dispatch
[:activity-center.contact-verification/decline id])
(rf/dispatch
[:activity-center.notifications/mark-as-read id]))}
:on-press #(decline-challenge id)}
(if replying?
{:type :button
:subtype :primary
:key :button-reply
:label (i18n/label :t/send-reply)
:accessibility-label :reply-to-contact-verification
:disable-when #(not (valid-reply? %))
:on-press (fn []
(hide-bottom-sheet-and-dispatch
[:activity-center.contact-verification/reply id
@reply])
(rf/dispatch
[:activity-center.notifications/mark-as-read id]))}
:disable-when invalid-reply?
:on-press #(send-challenge-reply id @reply)}
{:type :button
:subtype :primary
:key :button-send-reply
:label (i18n/label :t/message-reply)
:accessibility-label :send-reply-to-contact-verification
:on-press (fn []
(rf/dispatch [:bottom-sheet/show-sheet
{:content view}
{:notification notification
:replying? true}]))})]))})])))))
:on-press #(prepare-challenge-reply props)})]))})]])))))

View File

@ -1,26 +1,65 @@
(ns status-im2.contexts.activity-center.notification.membership.view
(:require [quo2.core :as quo]
[react-native.core :as rn]
[react-native.gesture :as gesture]
[status-im2.contexts.activity-center.notification.common.style :as common-style]
[status-im2.contexts.activity-center.notification.common.view :as common]
[utils.datetime :as datetime]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn pressable
[{:keys [accepted chat-id]} & children]
(defn- pressable
[{:keys [accepted chat-id]} child]
(if accepted
(into [rn/touchable-opacity
[gesture/touchable-without-feedback
{:on-press (fn []
(rf/dispatch [:hide-popover])
(rf/dispatch [:chat/navigate-to-chat chat-id]))}]
children)
(into [:<>] children)))
(rf/dispatch [:chat/navigate-to-chat chat-id]))}
child]
child))
(defn- swipe-button-accept
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-success-container style)
:icon :i/placeholder
:text (i18n/label :t/accept)}])
(defn- swipe-button-decline
[{:keys [style]} _]
[common/swipe-button-container
{:style (common-style/swipe-danger-container style)
:icon :i/placeholder
:text (i18n/label :t/decline)}])
(defn- swipeable
[{:keys [active-swipeable notification extra-fn]} child]
(let [{:keys [accepted dismissed id]} notification]
(if (or accepted dismissed)
[common/swipeable
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn extra-fn}
child]
[common/swipeable
{:left-button swipe-button-accept
:left-on-press #(rf/dispatch [:activity-center.notifications/accept id])
:right-button swipe-button-decline
:right-on-press #(rf/dispatch [:activity-center.notifications/dismiss id])
:active-swipeable active-swipeable
:extra-fn extra-fn}
child])))
(defn view
[{:keys [id accepted author read timestamp chat-name chat-id]}]
[{:keys [notification set-swipeable-height] :as props}]
(let [{:keys [id accepted dismissed author read timestamp chat-name chat-id]} notification]
[swipeable props
[pressable {:accepted accepted :chat-id chat-id}
[quo/activity-log
{:title (i18n/label :t/added-to-group-chat)
:on-layout set-swipeable-height
:icon :i/add-user
:timestamp (datetime/timestamp->relative timestamp)
:unread? (not read)
@ -29,7 +68,7 @@
[quo/group-avatar-tag chat-name
{:size :small
:color :purple}]]
:items (when-not accepted
:items (when-not (or accepted dismissed)
[{:type :button
:subtype :positive
:key :button-accept
@ -42,4 +81,4 @@
:label (i18n/label :t/decline)
:accessibility-label :decline-group-chat-invitation
:on-press #(rf/dispatch [:activity-center.notifications/dismiss
id])}])}]])
id])}])}]]]))

View File

@ -9,14 +9,14 @@
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(def tag-params
(def ^:private tag-params
{:size :small
:override-theme :dark
:color colors/primary-50
:style style/tag
:text-style style/tag-text})
(defn message-body
(defn- message-body
[message]
(let [parsed-text (get-in message [:content :parsed-text])
parsed-text-children (:children (first parsed-text))]
@ -35,24 +35,26 @@
literal))
parsed-text-children))))
(defn swipeable
[{:keys [height active-swipeable notification]} child]
(defn- swipeable
[{:keys [active-swipeable extra-fn]} child]
[common/swipeable
{:left-button common/left-swipe-button
:left-on-press common/left-swipe-on-press
:right-button common/right-swipe-button
:right-on-press common/right-swipe-on-press
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn (fn [] {:height @height :notification notification})}
:extra-fn extra-fn}
child])
(defn view
[{:keys [author chat-name community-id chat-id message read timestamp]}
set-swipeable-height]
(let [community-chat? (not (string/blank? community-id))
[{:keys [notification set-swipeable-height] :as props}]
(let [{:keys [author chat-name community-id chat-id
message read timestamp]} notification
community-chat? (not (string/blank? community-id))
community (rf/sub [:communities/community community-id])
community-name (:name community)
community-image (get-in community [:images :thumbnail :uri])]
[swipeable props
[gesture/touchable-without-feedback
{:on-press (fn []
(rf/dispatch [:hide-popover])
@ -68,4 +70,4 @@
(if community-chat?
[quo/context-tag tag-params {:uri community-image} community-name chat-name]
[quo/group-avatar-tag chat-name tag-params])]
:message {:body (message-body message)}}]]))
:message {:body (message-body message)}}]]]))

View File

@ -12,7 +12,7 @@
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(def tag-params
(def ^:private tag-params
{:size :small
:override-theme :dark
:color colors/primary-50
@ -20,7 +20,7 @@
:text-style style/tag-text})
;; NOTE: Replies support text, image and stickers only.
(defn get-message-content
(defn- get-message-content
[{:keys [content-type] :as message}]
(case content-type
constants/content-type-text (get-in message [:content :text])
@ -37,24 +37,26 @@
nil))
(defn swipeable
[{:keys [height active-swipeable notification]} child]
(defn- swipeable
[{:keys [active-swipeable extra-fn]} child]
[common/swipeable
{:left-button common/left-swipe-button
:left-on-press common/left-swipe-on-press
:right-button common/right-swipe-button
:right-on-press common/right-swipe-on-press
{:left-button common/swipe-button-read-or-unread
:left-on-press common/swipe-on-press-toggle-read
:right-button common/swipe-button-delete
:right-on-press common/swipe-on-press-delete
:active-swipeable active-swipeable
:extra-fn (fn [] {:height @height :notification notification})}
:extra-fn extra-fn}
child])
(defn view
[{:keys [author chat-name community-id chat-id message read timestamp]}
set-swipeable-height]
(let [community-chat? (not (string/blank? community-id))
[{:keys [notification set-swipeable-height] :as props}]
(let [{:keys [author chat-name community-id chat-id
message read timestamp]} notification
community-chat? (not (string/blank? community-id))
community (rf/sub [:communities/community community-id])
community-name (:name community)
community-image (get-in community [:images :thumbnail :uri])]
[swipeable props
[gesture/touchable-without-feedback
{:on-press (fn []
(rf/dispatch [:hide-popover])
@ -71,4 +73,4 @@
[quo/context-tag tag-params {:uri community-image} community-name chat-name]
[quo/group-avatar-tag chat-name tag-params])]
:message {:body-number-of-lines 1
:body (get-message-content message)}}]]))
:body (get-message-content message)}}]]]))

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.activity-center.view
(:require [oops.core :as oops]
(:require [clojure.set :as set]
[oops.core :as oops]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
@ -127,7 +128,7 @@
:label (i18n/label :t/membership)
:accessibility-label :tab-membership
:notification-dot? (when-not is-mark-all-as-read-undoable?
(contains? types-with-unread types/membership))}
(set/subset? types/membership types-with-unread))}
{:id types/system
:label (i18n/label :t/system)
:accessibility-label :tab-system
@ -172,32 +173,30 @@
(let [height (atom 0)
set-swipeable-height #(reset! height (oops/oget % "nativeEvent.layout.height"))]
(fn [{:keys [type] :as notification} index _ active-swipeable]
(let [swipeable-args {:height height
(let [props {:height height
:active-swipeable active-swipeable
:notification notification}]
:set-swipeable-height set-swipeable-height
:notification notification
:extra-fn (fn [] {:height @height :notification notification})}]
[rn/view {:style (style/notification-container index)}
(cond
(= type types/contact-verification)
[contact-verification/view notification {}]
[contact-verification/view props]
(= type types/contact-request)
[contact-requests/swipeable swipeable-args
[contact-requests/view notification set-swipeable-height]]
[contact-requests/view props]
(= type types/mention)
[mentions/swipeable swipeable-args
[mentions/view notification set-swipeable-height]]
[mentions/view props]
(= type types/reply)
[reply/swipeable swipeable-args
[reply/view notification set-swipeable-height]]
[reply/view props]
(= type types/admin)
[admin/swipeable swipeable-args
[admin/view notification set-swipeable-height]]
[admin/view props]
(some types/membership [type])
[membership/view notification]
[membership/view props]
:else
nil)]))))

View File

@ -286,7 +286,7 @@ class TestActivityMultipleDevicePR(MultipleSharedDeviceTestCase):
self.home_1.just_fyi("Mark it as read and check filter")
reply_element.swipe_right_on_element()
self.home_1.activity_left_swipe_button.click()
self.home_1.activity_notification_swipe_button.click()
if reply_element.is_element_displayed(2):
self.errors.append("Message is not marked as read!")
self.home_1.activity_unread_filter_button.click()
@ -295,7 +295,7 @@ class TestActivityMultipleDevicePR(MultipleSharedDeviceTestCase):
self.home_1.just_fyi("Mark it as unread and check filter via right swipe")
reply_element.swipe_right_on_element()
self.home_1.activity_left_swipe_button.click()
self.home_1.activity_notification_swipe_button.click()
if not reply_element.unread_indicator.is_element_displayed():
self.errors.append("No unread dot is shown on activity center element after marking it as unread!")
@ -310,7 +310,7 @@ class TestActivityMultipleDevicePR(MultipleSharedDeviceTestCase):
self.home_1.just_fyi("Delete it from unread via left swipe")
self.home_1.open_activity_center_button.click()
reply_element.swipe_left_on_element()
self.home_1.activity_right_swipe_button.click()
self.home_1.activity_notification_swipe_button.click()
if reply_element.is_element_displayed():
self.errors.append("Reply is still shown after removing from activity centre!")

View File

@ -254,8 +254,7 @@ class HomeView(BaseView):
# Activity centre
self.mention_activity_tab_button = ActivityTabButton(self.driver, accessibility_id="tab-mention")
self.reply_activity_tab_button = ActivityTabButton(self.driver, accessibility_id="tab-reply")
self.activity_right_swipe_button = Button(self.driver, accessibility_id="notification-right-swipe")
self.activity_left_swipe_button = Button(self.driver, accessibility_id="notification-left-swipe")
self.activity_notification_swipe_button = Button(self.driver, accessibility_id="notification-swipe")
self.activity_unread_filter_button = Button(self.driver, accessibility_id="selector-filter")