From b774ecbcb4089e749f462c4cc35f5f7e9d4a3879 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 26 Oct 2022 17:08:40 +0100 Subject: [PATCH] Add chat actions in home screen This commit adds the following chat actions on the home screen: - Mute chat - Delete chat - View profile (one to ones only) - Clear history It adds also integration tests for muting and deleting a chat. To accommodate multiple dividers in the bottom sheet, the interface has been changed to accept a sequence of sequences, instead of a map. --- .../components/drawers/action_drawers.cljs | 114 +++++++++-------- src/quo2/screens/drawers/action_drawers.cljs | 35 +++--- src/status_im/chat/models.cljs | 20 ++- src/status_im/integration_test.cljs | 61 +++++++++ src/status_im/ui2/screens/chat/actions.cljs | 117 ++++++++++++++++++ src/status_im/ui2/screens/chat/home.cljs | 8 +- translations/en.json | 3 + 7 files changed, 279 insertions(+), 79 deletions(-) create mode 100644 src/status_im/ui2/screens/chat/actions.cljs diff --git a/src/quo2/components/drawers/action_drawers.cljs b/src/quo2/components/drawers/action_drawers.cljs index 1480ccdefa..f94f1e2f08 100644 --- a/src/quo2/components/drawers/action_drawers.cljs +++ b/src/quo2/components/drawers/action_drawers.cljs @@ -5,64 +5,70 @@ [quo2.components.icon :as icon] [quo2.foundations.colors :as colors])) -(defn- get-icon-color [section] - (if (= section :bottom) +(defn- get-icon-color [danger?] + (if danger? (colors/theme-colors colors/danger-50 colors/danger-60) (colors/theme-colors colors/neutral-50 colors/neutral-40))) -(defn action [section] - (fn [{:keys [icon label sub-label right-icon on-press]}] - [rn/touchable-opacity {:on-press on-press} - [react/view {:style - {:flex 1 - :height (if sub-label 56 47) - :margin-horizontal 20 - :flex-direction :row}} - [react/view {:style - {:height 20 - :margin-top :auto - :margin-bottom :auto - :margin-right 12 - :width 20}} - [icon/icon icon - {:color (get-icon-color section) - :size 20}]] - [react/view - {:style - {:flex 1 - :justify-content :center}} +(defn action [{:keys [icon + label + sub-label + right-icon + danger? + on-press]}] + [rn/touchable-opacity {:on-press on-press} + [react/view {:style + {:height (if sub-label 56 47) + :margin-horizontal 20 + :flex-direction :row}} + [react/view {:style + {:height 20 + :margin-top :auto + :margin-bottom :auto + :margin-right 12 + :width 20}} + [icon/icon icon + {:color (get-icon-color danger?) + :size 20}]] + [react/view + {:style + {:flex 1 + :justify-content :center}} + [text/text + {:size :paragraph-1 + :weight :medium + :style {:color + (when danger? + (colors/theme-colors colors/danger-50 colors/danger-60))}} + label] + (when sub-label [text/text - {:size :paragraph-1 - :weight :medium - :style - {:color (when (= section :bottom) - (colors/theme-colors colors/danger-50 colors/danger-60))}} - label] - (when sub-label [text/text - {:size :paragraph-2 - :style - {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}} - sub-label])] - (when right-icon - [react/view {:style - {:height 20 - :margin-top :auto - :margin-bottom :auto - :width 20}} - [icon/icon right-icon - {:color (get-icon-color section) - :size 20}]])]])) + {:size :paragraph-2 + :style {:color + (colors/theme-colors colors/neutral-50 colors/neutral-40)}} + sub-label])] + (when right-icon + [react/view {:style + {:height 20 + :margin-top :auto + :margin-bottom :auto + :width 20}} + [icon/icon right-icon + {:color (get-icon-color danger?) + :size 20}]])]]) -(defn action-drawer [{:keys [actions actions-with-consequence]}] +(defn divider [] + [rn/view {:style {:border-top-width 1 + :border-top-color (colors/theme-colors colors/neutral-10 colors/neutral-80) + :margin-top 8 + :margin-bottom 7 + :align-items :center + :flex-direction :row}}]) + +(defn action-drawer [sections] [:<> {:style {:flex 1}} - (map (action :top) actions) - (when actions-with-consequence - [:<> - [rn/view {:style {:border-top-width 1 - :border-top-color (colors/theme-colors colors/neutral-10 colors/neutral-80) - :margin-top 8 - :margin-bottom 7 - :align-items :center - :flex-direction :row}}] - (map (action :bottom) actions-with-consequence)])]) + (interpose + [divider] + (for [actions sections] + (map action actions)))]) diff --git a/src/quo2/screens/drawers/action_drawers.cljs b/src/quo2/screens/drawers/action_drawers.cljs index 8c7d19a397..ffc080847b 100644 --- a/src/quo2/screens/drawers/action_drawers.cljs +++ b/src/quo2/screens/drawers/action_drawers.cljs @@ -14,24 +14,27 @@ :key :show-red-options? :type :boolean}]) -(def options-with-consequences [{:icon :main-icons2/share-context - :label "Clear history"}]) +(def options-with-consequences [{:icon :main-icons2/delete + :danger? true + :label "Clear history"}]) (defn render-action-sheet [state] - [quo2/action-drawer {:actions-with-consequence (when (:show-red-options? @state) options-with-consequences) - :actions [{:icon :main-icons2/share-context - :label "View channel members and details"} - {:icon :main-icons2/communities - :label "Mark as read"} - {:icon :main-icons2/muted - :label (if (:muted? @state) "Unmute channel" "Mute channel") - :right-icon :main-icons2/chevron-right - :sub-label (when (:muted? @state) "Muted for 15 min")} - {:icon :main-icons2/scan - :right-icon :main-icons2/chevron-right - :label "Fetch messages"} - {:icon :main-icons2/add-user - :label "Share link to the channel"}]}]) + [quo2/action-drawer (cond-> [[{:icon :main-icons2/friend + :label "View channel members and details"} + {:icon :main-icons2/communities + :label "Mark as read"} + {:icon :main-icons2/muted + :label (if (:muted? @state) "Unmute channel" "Mute channel") + :right-icon :main-icons2/chevron-right + :sub-label (when (:muted? @state) "Muted for 15 min")} + {:icon :main-icons2/scan + :right-icon :main-icons2/chevron-right + :label "Fetch messages"} + {:icon :main-icons2/add-user + :label "Share link to the channel"}]] + + (:show-red-options? @state) + (conj options-with-consequences))]) (defn cool-preview [] (let [state (reagent/atom {:muted? true diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index ddbefa4a85..4edf5e4b33 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -176,6 +176,11 @@ :on-error #(log/error "failed to clear history " chat-id %)}]} (clear-history chat-id remove-chat?))) +(fx/defn chat-deactivated + {:events [::chat-deactivated]} + [_ chat-id] + (log/debug "chat deactivated" chat-id)) + (fx/defn deactivate-chat "Deactivate chat in db, no side effects" [{:keys [db now] :as cofx} chat-id] @@ -185,10 +190,10 @@ (assoc-in db [:chats chat-id :active] false) (update db :chats dissoc chat-id)) (update :chats-home-list disj chat-id) - (assoc-in [:current-chat-id] nil)) + (assoc :current-chat-id nil)) ::json-rpc/call [{:method "wakuext_deactivateChat" :params [{:id chat-id}] - :on-success #(log/debug "chat deactivated" chat-id) + :on-success #(re-frame/dispatch [::chat-deactivated chat-id]) :on-error #(log/error "failed to create public chat" chat-id %)}]} (clear-history chat-id true))) @@ -227,9 +232,7 @@ {:clear-message-notifications [[chat-id] (get-in db [:multiaccount :remote-push-notifications-enabled?])]} (deactivate-chat chat-id) - (offload-messages chat-id) - (when (not (= (:view-id db) :home)) - (navigation/pop-to-root-tab :chat-stack)))) + (offload-messages chat-id))) (fx/defn show-more-chats {:events [:chat.ui/show-more-chats]} @@ -385,6 +388,11 @@ (log/error "mute chat failed" chat-id error) {:db (assoc-in db [:chats chat-id :muted] (not muted?))}) +(fx/defn mute-chat-toggled-successfully + {:events [::mute-chat-toggled-successfully]} + [_ chat-id] + (log/debug "muted chat successfully" chat-id)) + (fx/defn mute-chat {:events [::mute-chat-toggled]} [{:keys [db] :as cofx} chat-id muted?] @@ -393,7 +401,7 @@ ::json-rpc/call [{:method method :params [chat-id] :on-error #(re-frame/dispatch [::mute-chat-failed chat-id muted? %]) - :on-success #(log/debug "muted chat successfully")}]})) + :on-success #(re-frame/dispatch [::mute-chat-toggled-successfully chat-id])}]})) (fx/defn show-profile {:events [:chat.ui/show-profile]} diff --git a/src/status_im/integration_test.cljs b/src/status_im/integration_test.cljs index 806a58dd02..22fdd61c5c 100644 --- a/src/status_im/integration_test.cljs +++ b/src/status_im/integration_test.cljs @@ -4,6 +4,7 @@ [clojure.string :as string] [re-frame.core :as rf] status-im.events + [status-im.chat.models :as chat.models] [status-im.utils.security :as security] [status-im.multiaccounts.logout.core :as logout] [status-im.transport.core :as transport] @@ -231,5 +232,65 @@ (logout!) (rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in an inconsistent state between tests (assert-logout)))))))) +(deftest delete-chat-test + (log/info "========= delete-chat-test ==================") + (rf-test/run-test-async + (initialize-app!) + (rf-test/wait-for + [:status-im.init.core/initialize-view] + (generate-and-derive-addresses!) + (rf-test/wait-for + [:multiaccount-generate-and-derive-addresses-success] ; wait for the keys + (assert-multiaccount-loaded) + (create-multiaccount!) + (rf-test/wait-for + [::transport/messenger-started] + (assert-messenger-started) + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat + (rf-test/wait-for + [:status-im.chat.models/one-to-one-chat-created] + (rf/dispatch-sync [:chat.ui/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + (rf/dispatch-sync [:chat.ui/remove-chat-pressed chat-id]) + (rf/dispatch-sync [:chat.ui/remove-chat chat-id]) + (rf-test/wait-for + [::chat.models/chat-deactivated] + (is (not @(rf/subscribe [:chats/chat chat-id]))) + (logout!) (rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in an inconsistent state between tests + (assert-logout))))))))) + +(deftest mute-chat-test + (log/info "========= mute-chat-test ==================") + (rf-test/run-test-async + (initialize-app!) + (rf-test/wait-for + [:status-im.init.core/initialize-view] + (generate-and-derive-addresses!) + (rf-test/wait-for + [:multiaccount-generate-and-derive-addresses-success] ; wait for the keys + (assert-multiaccount-loaded) + (create-multiaccount!) + (rf-test/wait-for + [::transport/messenger-started] + (assert-messenger-started) + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat + (rf-test/wait-for + [:status-im.chat.models/one-to-one-chat-created] + (rf/dispatch-sync [:chat.ui/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + (rf/dispatch-sync [::chat.models/mute-chat-toggled chat-id true]) + (rf-test/wait-for + [::chat.models/mute-chat-toggled-successfully] + (is @(rf/subscribe [:chats/muted chat-id])) + (rf/dispatch-sync [::chat.models/mute-chat-toggled chat-id false]) + (rf-test/wait-for + [::chat.models/mute-chat-toggled-successfully] + + (is (not @(rf/subscribe [:chats/muted chat-id]))) + (logout!) (rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in an inconsistent state between tests + (assert-logout)))))))))) + (comment (run-tests)) diff --git a/src/status_im/ui2/screens/chat/actions.cljs b/src/status_im/ui2/screens/chat/actions.cljs new file mode 100644 index 0000000000..7261506a82 --- /dev/null +++ b/src/status_im/ui2/screens/chat/actions.cljs @@ -0,0 +1,117 @@ +(ns status-im.ui2.screens.chat.actions + (:require + [status-im.chat.models :as chat.models] + [status-im.chat.models.pin-message :as models.pin-message] + [status-im.i18n.i18n :as i18n] + [status-im.constants :as constants] + [status-im.utils.handlers :refer [evt]] + [quo2.components.drawers.action-drawers :as drawer])) + +(defn- entry [icon label on-press danger?] + {:pre [(keyword? icon) + (string? label) + (fn? on-press) + (boolean? danger?)]} + {:icon icon + :label label + :on-press on-press + :danger? danger?}) + +(defn hide-sheet-and-dispatch [event] + (>evt [:bottom-sheet/hide]) + (>evt event)) + +(defn show-profile-action [chat-id] + (hide-sheet-and-dispatch [:chat.ui/show-profile chat-id]) + (>evt [::models.pin-message/load-pin-messages chat-id])) + +(defn mark-all-read-action [chat-id] + (hide-sheet-and-dispatch [:chat/mark-all-as-read chat-id])) + +(defn edit-nickname-action [chat-id] + (hide-sheet-and-dispatch [:chat.ui/edit-nickname chat-id])) + +(defn mute-chat-action [chat-id] + (hide-sheet-and-dispatch [::chat.models/mute-chat-toggled chat-id true])) + +(defn unmute-chat-action [chat-id] + (hide-sheet-and-dispatch [::chat.models/mute-chat-toggled chat-id false])) + +(defn clear-history-action [chat-id] + (hide-sheet-and-dispatch [:chat.ui/clear-history-pressed chat-id])) + +(defn delete-chat-action [chat-id] + (hide-sheet-and-dispatch [:chat.ui/remove-chat-pressed chat-id])) + +(defn mute-chat-entry [muted? chat-id] + (entry :main-icons2/muted + (i18n/label + (if muted? + :unmute-chat + :mute-chat)) + (if muted? + #(unmute-chat-action chat-id) + #(mute-chat-action chat-id)) + false)) + +(defn mark-as-read-entry [chat-id] + (entry :main-icons2/check + (i18n/label :mark-as-read) + #(mark-all-read-action chat-id) + false)) + +(defn clear-history-entry [chat-id] + (entry :main-icons2/delete + (i18n/label :clear-history) + #(clear-history-action chat-id) + true)) + +(defn delete-chat-entry [chat-id] + (entry :main-icons2/delete + (i18n/label :delete-chat) + #(delete-chat-action chat-id) + true)) + +(defn view-profile-entry [chat-id] + (entry :main-icons2/friend + (i18n/label :view-profile) + #(show-profile-action chat-id) + false)) + +(defn edit-nickname-entry [chat-id] + (entry :main-icons2/edit + (i18n/label :edit-nickname) + #(edit-nickname-action chat-id) + false)) + +(defn destructive-actions [chat-id] + [(clear-history-entry chat-id) + (delete-chat-entry chat-id)]) + +(defn notification-actions [muted? chat-id] + [(mute-chat-entry muted? chat-id) + (mark-as-read-entry chat-id)]) + +(defn one-to-one-actions [muted? chat-id] + [drawer/action-drawer [[(view-profile-entry chat-id) + (edit-nickname-entry chat-id)] + (notification-actions muted? chat-id) + (destructive-actions chat-id)]]) + +(defn public-chat-actions [muted? chat-id] + [drawer/action-drawer [(notification-actions muted? chat-id) + (destructive-actions chat-id)]]) + +(defn private-group-chat-actions [muted? chat-id] + [drawer/action-drawer [(notification-actions muted? chat-id) + (destructive-actions chat-id)]]) + +(defn actions [chat-type chat-id] + (let [muted? (evt]] @@ -117,7 +117,7 @@ (re-frame/dispatch [:set :public-group-topic nil]) (re-frame/dispatch [:search/home-filter-changed nil]))}])]))) -(defn render-fn [{:keys [chat-id] :as home-item}] +(defn render-fn [{:keys [chat-id chat-type] :as home-item}] [home-list-item home-item {:on-press (fn [] @@ -129,7 +129,9 @@ (re-frame/dispatch [:accept-all-activity-center-notifications-from-chat chat-id])) :on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet {:content (fn [] - [sheets/actions home-item])}])}]) + [actions/actions + chat-type + chat-id])}])}]) (defn- render-contact [{:keys [public-key] :as row}] (let [[first-name second-name] (multiaccounts/contact-two-names row true) diff --git a/translations/en.json b/translations/en.json index d61e3a3798..58b5a6e705 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1480,6 +1480,7 @@ "page-camera-request-blocked": "camera requests blocked. To enable camera requests go to Settings", "nickname": "Nickname", "add-nickname": "Add a nickname (optional)", + "edit-nickname": "Edit nickname", "nickname-description": "Nicknames help you identify others in Status.\nOnly you can see the nicknames you’ve added", "accept": "Accept", "group-invite": "Group invite", @@ -1510,6 +1511,8 @@ "name-optional": "Name (optional)", "mute": "Mute", "unmute": "Unmute", + "mute-chat": "Mute chat", + "unmute-chat": "Unmute chat", "mute-community": "Mute community", "unmute-community": "Unmute community", "scan-tokens": "Scan tokens",