From 9cfb59106895a03417b3254060abf42d27d61d91 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Mon, 14 May 2018 17:32:44 +0200 Subject: [PATCH] Remove chat / clear history Signed-off-by: Andrea Maria Piana --- src/status_im/chat/events.cljs | 46 ++++---- src/status_im/chat/models.cljs | 67 +++++++++--- src/status_im/chat/models/group_chat.cljs | 77 +++++++++++++ src/status_im/chat/models/message.cljs | 16 +-- src/status_im/chat/views/actions.cljs | 11 +- src/status_im/core.cljs | 1 + src/status_im/data_store/chats.cljs | 8 ++ .../realm/schemas/account/core.cljs | 12 ++- .../realm/schemas/account/v3/chat.cljs | 34 ++++++ .../realm/schemas/account/v3/core.cljs | 27 +++++ src/status_im/translations/en.cljs | 2 +- src/status_im/transport/core.cljs | 2 - src/status_im/transport/handlers.cljs | 2 +- src/status_im/transport/impl/receive.cljs | 34 ++++++ .../transport/message/v1/contact.cljs | 18 +--- .../transport/message/v1/group_chat.cljs | 77 +------------ .../screens/group/chat_settings/events.cljs | 17 +-- .../ui/screens/profile/group_chat/views.cljs | 12 +-- test/cljs/status_im/test/chat/models.cljs | 102 ++++++++++++++++++ .../status_im/test/chat/models/message.cljs | 22 ++++ test/cljs/status_im/test/runner.cljs | 2 + .../status_im/test/utils/handlers_macro.cljs | 32 ++++++ 22 files changed, 444 insertions(+), 177 deletions(-) create mode 100644 src/status_im/chat/models/group_chat.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/v3/chat.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/v3/core.cljs create mode 100644 src/status_im/transport/impl/receive.cljs create mode 100644 test/cljs/status_im/test/utils/handlers_macro.cljs diff --git a/src/status_im/chat/events.cljs b/src/status_im/chat/events.cljs index 049ea84c0b..2dd1888899 100644 --- a/src/status_im/chat/events.cljs +++ b/src/status_im/chat/events.cljs @@ -342,38 +342,15 @@ (fn [cofx [chat]] (models/upsert-chat chat cofx))) -(defn- remove-transport [chat-id {:keys [db] :as cofx}] - (let [{:keys [group-chat public?]} (get-in db [:chats chat-id])] - ;; if this is private group chat, we have to broadcast leave and unsubscribe after that - (if (and group-chat (not public?)) - (handlers-macro/merge-fx cofx (transport.message/send (group-chat/GroupLeave.) chat-id)) - (handlers-macro/merge-fx cofx (transport/unsubscribe-from-chat chat-id))))) - -(handlers/register-handler-fx - :leave-chat-and-navigate-home - [re-frame/trim-v] - (fn [cofx [chat-id]] - (handlers-macro/merge-fx cofx - (models/remove-chat chat-id) - (navigation/replace-view :home) - (remove-transport chat-id)))) - -(handlers/register-handler-fx - :leave-group-chat? - [re-frame/trim-v] - (fn [_ [chat-id]] - {:show-confirmation {:title (i18n/label :t/leave-confirmation) - :content (i18n/label :t/leave-group-chat-confirmation) - :confirm-button-text (i18n/label :t/leave) - :on-accept #(re-frame/dispatch [:leave-chat-and-navigate-home chat-id])}})) +(defn remove-chat-and-navigate-home [cofx [chat-id]] + (handlers-macro/merge-fx cofx + (models/remove-chat chat-id) + (navigation/replace-view :home))) (handlers/register-handler-fx :remove-chat-and-navigate-home [re-frame/trim-v] - (fn [cofx [chat-id]] - (handlers-macro/merge-fx cofx - (models/remove-chat chat-id) - (navigation/replace-view :home)))) + remove-chat-and-navigate-home) (handlers/register-handler-fx :remove-chat-and-navigate-home? @@ -384,6 +361,19 @@ :confirm-button-text (i18n/label :t/delete) :on-accept #(re-frame/dispatch [:remove-chat-and-navigate-home chat-id])}})) +(handlers/register-handler-fx + :clear-history + (fn [{{:keys [current-chat-id]} :db :as cofx} _] + (models/clear-history current-chat-id cofx))) + +(handlers/register-handler-fx + :clear-history? + (fn [_ _] + {:show-confirmation {:title (i18n/label :t/clear-history-confirmation) + :content (i18n/label :t/clear-history-confirmation-content) + :confirm-button-text (i18n/label :t/clear) + :on-accept #(re-frame/dispatch [:clear-history])}})) + (handlers/register-handler-fx :create-new-public-chat [re-frame/trim-v] diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index 6aca84ae0b..ef5aba660d 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -1,9 +1,21 @@ (ns status-im.chat.models (:require [status-im.ui.components.styles :as styles] [status-im.utils.gfycat.core :as gfycat] + [status-im.transport.utils :as transport.utils] + [status-im.utils.clocks :as utils.clocks] + [status-im.transport.message.core :as transport.message] [status-im.data-store.chats :as chats-store] + [status-im.transport.message.v1.group-chat :as transport.group-chat] + [status-im.utils.handlers-macro :as handlers-macro] [status-im.data-store.messages :as messages-store])) +(defn multi-user-chat? [chat-id cofx] + (get-in cofx [:db :chats chat-id :group-chat])) + +(defn group-chat? [chat-id cofx] + (and (multi-user-chat? chat-id cofx) + (not (get-in cofx [:db :chats chat-id :public?])))) + (defn set-chat-ui-props "Updates ui-props in active chat by merging provided kvs into them" [{:keys [current-chat-id] :as db} kvs] @@ -60,21 +72,50 @@ :group-admin admin :contacts participants} cofx)) -(defn new-update? [{:keys [added-to-at removed-at removed-from-at]} timestamp] - (and (> timestamp added-to-at) - (> timestamp removed-at) - (> timestamp removed-from-at))) +(defn clear-history [chat-id {:keys [db] :as cofx}] + (let [{:keys [messages + deleted-at-clock-value]} (get-in db [:chats chat-id]) + last-message-clock-value (or (->> messages + vals + (sort-by (comp unchecked-negate :clock-value)) + first + :clock-value) + deleted-at-clock-value + (utils.clocks/send 0))] + ;; Necessary until we adjust merge-fx to cater for :txs + (-> (select-keys cofx [:data-store/tx :db]) + (assoc-in [:db :chats chat-id :messages] {}) + (assoc-in [:db :chats chat-id :message-groups] {}) + (assoc-in [:db :chats chat-id :deleted-at-clock-value] last-message-clock-value) + (update :data-store/tx concat [(chats-store/clear-history-tx chat-id last-message-clock-value) + (messages-store/delete-messages-tx chat-id)])))) +(defn- remove-transport [chat-id {:keys [db] :as cofx}] + ;; if this is private group chat, we have to broadcast leave and unsubscribe after that + (if (group-chat? chat-id cofx) + (transport.message/send (transport.group-chat/GroupLeave.) chat-id cofx) + (transport.utils/unsubscribe-from-chat chat-id cofx))) + +(defn- deactivate-chat [chat-id {:keys [db now] :as cofx}] + (assoc-in {:db db + :data-store/tx [(chats-store/deactivate-chat-tx chat-id now)]} + [:db :chats chat-id :is-active] false)) + +;; TODO: There's a race condition here, as the removal of the filter (async) +;; is done at the same time as the removal of the chat, so a message +;; might come between and restore the chat. Multiple way to handle this +;; (remove chat only after the filter has been removed, probably the safest, +;; flag the chat to ignore new messages, change receive method for public/group chats) +;; For now to keep the code simplier and avoid significant changes, best to leave as it is. (defn remove-chat [chat-id {:keys [db now] :as cofx}] - (let [{:keys [chat-id group-chat debug?]} (get-in db [:chats chat-id])] - (if debug? - (-> {:db db} - (update-in [:db :chats] dissoc chat-id) - (assoc :data-store/tx [(chats-store/delete-chat-tx chat-id) - (messages-store/delete-messages-tx chat-id)])) - (-> {:db db} - (assoc-in [:db :chats chat-id :is-active] false) - (assoc :data-store/tx [(chats-store/deactivate-chat-tx chat-id now)]))))) + (letfn [(remove-transport-fx [chat-id cofx] + (when (multi-user-chat? chat-id cofx) + (remove-transport chat-id cofx)))] + (handlers-macro/merge-fx + cofx + (remove-transport-fx chat-id) + (deactivate-chat chat-id) + (clear-history chat-id)))) (defn bot-only-chat? [db chat-id] (let [{:keys [group-chat contacts]} (get-in db [:chats chat-id])] diff --git a/src/status_im/chat/models/group_chat.cljs b/src/status_im/chat/models/group_chat.cljs new file mode 100644 index 0000000000..46d999a018 --- /dev/null +++ b/src/status_im/chat/models/group_chat.cljs @@ -0,0 +1,77 @@ +(ns status-im.chat.models.group-chat + (:require + [clojure.set :as set] + [status-im.i18n :as i18n] + [status-im.transport.utils :as transport.utils] + [status-im.ui.screens.group.core :as group] + [status-im.chat.models :as models.chat] + [status-im.transport.message.core :as message] + [status-im.transport.message.v1.group-chat :as transport.group-chat] + [status-im.utils.handlers-macro :as handlers-macro] + [status-im.chat.models.message :as models.message])) + +(defn- participants-diff [existing-participants-set new-participants-set] + {:removed (set/difference existing-participants-set new-participants-set) + :added (set/difference new-participants-set existing-participants-set)}) + +(defn- prepare-system-message [admin-name added-participants removed-participants contacts] + (let [added-participants-names (map #(get-in contacts [% :name] %) added-participants) + removed-participants-names (map #(get-in contacts [% :name] %) removed-participants)] + (cond + (and (seq added-participants) (seq removed-participants)) + (str admin-name " " + (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names)) + " and " + (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names))) + + (seq added-participants) + (str admin-name " " (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names))) + + (seq removed-participants) + (str admin-name " " (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names)))))) + +(defn handle-group-admin-update [{:keys [chat-name participants]} chat-id signature {:keys [now db random-id] :as cofx}] + (let [me (:current-public-key db)] + ;; we have to check if we already have a chat, or it's a new one + (if-let [{:keys [group-admin contacts] :as chat} (get-in db [:chats chat-id])] + ;; update for existing group chat + (when (and (= signature group-admin) ;; make sure that admin is the one making changes + (not= (set contacts) (set participants))) ;; make sure it's actually changing something + (let [{:keys [removed added]} (participants-diff (set contacts) (set participants)) + admin-name (or (get-in db [:contacts/contacts group-admin :name]) + group-admin)] + (if (removed me) ;; we were removed + (handlers-macro/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id random-id now + (str admin-name " " (i18n/label :t/removed-from-chat)))) + (models.chat/upsert-chat {:chat-id chat-id + :removed-from-at now + :is-active false}) + (transport.utils/unsubscribe-from-chat chat-id)) + (handlers-macro/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id random-id now + (prepare-system-message admin-name + added + removed + (:contacts/contacts db)))) + (group/participants-added chat-id added) + (group/participants-removed chat-id removed))))) + ;; first time we hear about chat -> create it if we are among participants + (when (get (set participants) me) + (models.chat/add-group-chat chat-id chat-name signature participants cofx))))) + +(defn handle-group-leave [chat-id signature {:keys [db random-id now] :as cofx}] + (let [me (:current-public-key db) + participant-leaving-name (or (get-in db [:contacts/contacts signature :name]) + signature)] + (when (and + (not= signature me) + (get-in db [:chats chat-id])) ;; chat is present + (handlers-macro/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id random-id now + (str participant-leaving-name " " (i18n/label :t/left)))) + (group/participants-removed chat-id #{signature}) + (transport.group-chat/send-new-group-key nil chat-id))))) diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index f0dd8b0c68..c349937d71 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -106,10 +106,6 @@ (def ^:private- add-single-message (partial add-message false)) (def ^:private- add-batch-message (partial add-message true)) -(defn- prepare-chat [chat-id {:keys [db now] :as cofx}] - (chat-model/upsert-chat {:chat-id chat-id - :timestamp now} cofx)) - (defn- send-message-seen [chat-id message-id send-seen? cofx] (when send-seen? (transport/send (protocol/map->MessagesSeen {:message-ids #{message-id}}) chat-id cofx))) @@ -198,12 +194,18 @@ (#{:group-user-message :public-group-user-message} message-type)) (defn add-to-chat? - [{:keys [db]} {:keys [chat-id from message-id] :as message}] + [{:keys [db]} {:keys [chat-id + clock-value + from + message-id] :as message}] (let [{:keys [chats current-public-key]} db - {:keys [messages not-loaded-message-ids]} (get chats chat-id)] + {:keys [deleted-at-clock-value + messages + not-loaded-message-ids]} (get chats chat-id)] (when (not= from current-public-key) (not (or (get messages message-id) - (get not-loaded-message-ids message-id)))))) + (get not-loaded-message-ids message-id) + (>= deleted-at-clock-value clock-value)))))) (defn message-seen-by? [message user-pk] (= :seen (get-in message [:user-statuses user-pk]))) diff --git a/src/status_im/chat/views/actions.cljs b/src/status_im/chat/views/actions.cljs index 8538c325a5..446cda8285 100644 --- a/src/status_im/chat/views/actions.cljs +++ b/src/status_im/chat/views/actions.cljs @@ -18,24 +18,19 @@ {:label (i18n/label :t/delete-chat) :action #(re-frame/dispatch [:remove-chat-and-navigate-home? chat-id group?])}) -(defn- leave-group-chat [chat-id public?] - {:label (i18n/label (if public? :t/leave-public-chat :t/leave-group-chat)) - :action #(re-frame/dispatch [:leave-group-chat? chat-id])}) - (defn- chat-actions [chat-id] [(view-profile chat-id) + (clear-history) (delete-chat chat-id false)]) (defn- group-chat-actions [chat-id] [(group-info chat-id) (clear-history) - (delete-chat chat-id true) - (leave-group-chat chat-id false)]) + (delete-chat chat-id true)]) (defn- public-chat-actions [chat-id] [(clear-history) - (delete-chat chat-id true) - (leave-group-chat chat-id true)]) + (delete-chat chat-id true)]) (defn actions [group-chat? chat-id public?] (cond diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs index 46818abf2f..2151aab3a6 100644 --- a/src/status_im/core.cljs +++ b/src/status_im/core.cljs @@ -3,6 +3,7 @@ [status-im.ui.components.react :as react] [reagent.core :as reagent] [status-im.native-module.core :as status] + status-im.transport.impl.receive [taoensso.timbre :as log] [status-im.utils.config :as config] [status-im.react-native.js-dependencies :as js-dependencies] diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index 53bb9608a0..a2ca91fc32 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -37,6 +37,14 @@ (defn- get-chat-by-id [chat-id realm] (core/single (core/get-by-field realm :chat :chat-id chat-id))) +(defn clear-history-tx + "Returns tx function for clearing the history of chat" + [chat-id deleted-at-clock-value] + (fn [realm] + (let [chat (get-chat-by-id chat-id realm)] + (doto chat + (aset "deleted-at-clock-value" deleted-at-clock-value))))) + (defn deactivate-chat-tx "Returns tx function for deactivating chat" [chat-id now] diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs index d5976bd6fd..df3cb88759 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -1,6 +1,8 @@ (ns status-im.data-store.realm.schemas.account.core - (:require [status-im.data-store.realm.schemas.account.v1.core :as v1] - [status-im.data-store.realm.schemas.account.v2.core :as v2])) + (:require + [status-im.data-store.realm.schemas.account.v1.core :as v1] + [status-im.data-store.realm.schemas.account.v2.core :as v2] + [status-im.data-store.realm.schemas.account.v3.core :as v3])) ;; TODO(oskarth): Add failing test if directory vXX exists but isn't in schemas. @@ -10,5 +12,7 @@ :migration v1/migration} {:schema v2/schema :schemaVersion 2 - :migration v2/migration}]) - + :migration v2/migration} + {:schema v3/schema + :schemaVersion 3 + :migration v3/migration}]) diff --git a/src/status_im/data_store/realm/schemas/account/v3/chat.cljs b/src/status_im/data_store/realm/schemas/account/v3/chat.cljs new file mode 100644 index 0000000000..5c34a79bdf --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v3/chat.cljs @@ -0,0 +1,34 @@ +(ns status-im.data-store.realm.schemas.account.v3.chat + (:require [status-im.ui.components.styles :refer [default-chat-color]])) + +(def schema {:name :chat + :primaryKey :chat-id + :properties {:chat-id :string + :name :string + :color {:type :string + :default default-chat-color} + :group-chat {:type :bool + :indexed true} + :group-admin {:type :string + :optional true} + :is-active :bool + :timestamp :int + :contacts {:type "string[]"} + :removed-at {:type :int + :optional true} + :removed-from-at {:type :int + :optional true} + :deleted-at-clock-value {:type :int + :optional true} + :added-to-at {:type :int + :optional true} + :updated-at {:type :int + :optional true} + :message-overhead {:type :int + :default 0} + :contact-info {:type :string + :optional true} + :debug? {:type :bool + :default false} + :public? {:type :bool + :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v3/core.cljs b/src/status_im/data_store/realm/schemas/account/v3/core.cljs new file mode 100644 index 0000000000..b6446b3572 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v3/core.cljs @@ -0,0 +1,27 @@ +(ns status-im.data-store.realm.schemas.account.v3.core + (:require [status-im.data-store.realm.schemas.account.v3.chat :as chat] + [status-im.data-store.realm.schemas.account.v1.transport :as transport] + [status-im.data-store.realm.schemas.account.v1.contact :as contact] + [status-im.data-store.realm.schemas.account.v1.message :as message] + [status-im.data-store.realm.schemas.account.v1.request :as request] + [status-im.data-store.realm.schemas.account.v2.mailserver :as mailserver] + [status-im.data-store.realm.schemas.account.v1.user-status :as user-status] + [status-im.data-store.realm.schemas.account.v1.local-storage :as local-storage] + [status-im.data-store.realm.schemas.account.v1.browser :as browser] + [goog.object :as object] + [taoensso.timbre :as log] + [cljs.reader :as reader] + [clojure.string :as string])) + +(def schema [chat/schema + transport/schema + contact/schema + message/schema + request/schema + mailserver/schema + user-status/schema + local-storage/schema + browser/schema]) + +(defn migration [old-realm new-realm] + (log/debug "migrating v3 account database: " old-realm new-realm)) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index d7cf3668c6..b3e03a6940 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -296,7 +296,7 @@ :clear "Clear" :clear-history "Clear history" :clear-history-title "Clear history?" - :clear-group-history-confirmation "Are you sure you want to clear this group's chat history?" + :clear-history-confirmation-content "Are you sure you want to clear this chat history?" :clear-history-confirmation "Clear history?" :clear-history-action "Clear" :mute-notifications "Mute notifications" diff --git a/src/status_im/transport/core.cljs b/src/status_im/transport/core.cljs index 65005c93b6..cf2cfad4b8 100644 --- a/src/status_im/transport/core.cljs +++ b/src/status_im/transport/core.cljs @@ -63,8 +63,6 @@ (fn [js-error js-message] (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message chat-id]))))) -(def unsubscribe-from-chat transport.utils/unsubscribe-from-chat) - (defn stop-whisper "Stops whisper protocol by removing all existing shh filters It is necessary to remove the filters because status-go there isn't currently a logout feature in status-go diff --git a/src/status_im/transport/handlers.cljs b/src/status_im/transport/handlers.cljs index edb3e1a923..dfbe7dc2d4 100644 --- a/src/status_im/transport/handlers.cljs +++ b/src/status_im/transport/handlers.cljs @@ -113,7 +113,7 @@ :group/unsubscribe-from-chat [re-frame/trim-v] (fn [cofx [chat-id]] - (transport/unsubscribe-from-chat chat-id cofx))) + (transport.utils/unsubscribe-from-chat chat-id cofx))) (handlers/register-handler-fx :group/send-new-sym-key diff --git a/src/status_im/transport/impl/receive.cljs b/src/status_im/transport/impl/receive.cljs new file mode 100644 index 0000000000..1167d09c62 --- /dev/null +++ b/src/status_im/transport/impl/receive.cljs @@ -0,0 +1,34 @@ +(ns status-im.transport.impl.receive + (:require + [status-im.chat.models.group-chat :as models.group-chat] + [status-im.ui.screens.contacts.core :as contacts] + [status-im.transport.message.core :as message] + [status-im.transport.message.v1.contact :as transport.contact] + [status-im.transport.message.v1.group-chat :as transport.group-chat])) + +(extend-type transport.group-chat/GroupAdminUpdate + message/StatusMessage + (receive [this chat-id signature cofx] + (models.group-chat/handle-group-admin-update this chat-id signature cofx))) + +(extend-type transport.group-chat/GroupLeave + message/StatusMessage + (receive [this chat-id signature cofx] + (models.group-chat/handle-group-leave chat-id signature cofx))) + +(extend-type transport.contact/ContactRequest + message/StatusMessage + (receive [this chat-id signature cofx] + (contacts/receive-contact-request signature this cofx))) + +(extend-type transport.contact/ContactRequestConfirmed + message/StatusMessage + (receive [this chat-id signature cofx] + (contacts/receive-contact-request-confirmation signature this cofx))) + +(extend-type transport.contact/ContactUpdate + message/StatusMessage + (receive [this chat-id signature cofx] + (contacts/receive-contact-update chat-id + signature + this cofx))) diff --git a/src/status_im/transport/message/v1/contact.cljs b/src/status_im/transport/message/v1/contact.cljs index d417bb3e6b..ef06a9aff7 100644 --- a/src/status_im/transport/message/v1/contact.cljs +++ b/src/status_im/transport/message/v1/contact.cljs @@ -4,7 +4,6 @@ [status-im.transport.message.core :as message] [status-im.transport.message.v1.protocol :as protocol] [status-im.transport.utils :as transport.utils] - [status-im.ui.screens.contacts.core :as contacts] [status-im.utils.handlers-macro :as handlers-macro])) (defrecord NewContactKey [sym-key topic message] @@ -41,20 +40,14 @@ (handlers-macro/merge-fx cofx {:shh/get-new-sym-keys [{:web3 (:web3 db) :on-success on-success}]} - (protocol/init-chat chat-id topic)))) - (receive [this chat-id signature {:keys [db] :as cofx}] - (contacts/receive-contact-request signature this cofx))) + (protocol/init-chat chat-id topic))))) (defrecord ContactRequestConfirmed [name profile-image address fcm-token] message/StatusMessage (send [this chat-id cofx] (handlers-macro/merge-fx cofx (protocol/send {:chat-id chat-id - :payload this}))) - (receive [this chat-id signature cofx] - (handlers-macro/merge-fx cofx - (contacts/receive-contact-request-confirmation signature - this)))) + :payload this})))) (defrecord ContactUpdate [name profile-image] message/StatusMessage @@ -62,9 +55,4 @@ (let [public-keys (remove nil? (map :public-key (vals (:contacts/contacts db))))] (handlers-macro/merge-fx cofx (protocol/multi-send-by-pubkey {:public-keys public-keys - :payload this})))) - (receive [this chat-id signature cofx] - (handlers-macro/merge-fx cofx - (contacts/receive-contact-update chat-id - signature - this)))) + :payload this}))))) diff --git a/src/status_im/transport/message/v1/group_chat.cljs b/src/status_im/transport/message/v1/group_chat.cljs index b29a28f876..1d2af177d5 100644 --- a/src/status_im/transport/message/v1/group_chat.cljs +++ b/src/status_im/transport/message/v1/group_chat.cljs @@ -1,13 +1,8 @@ (ns ^{:doc "Group chat API"} status-im.transport.message.v1.group-chat - (:require [clojure.set :as set] - [re-frame.core :as re-frame] + (:require [re-frame.core :as re-frame] [status-im.utils.handlers-macro :as handlers-macro] [status-im.transport.message.core :as message] - [status-im.i18n :as i18n] - [status-im.ui.screens.group.core :as group] - [status-im.chat.models :as models.chat] - [status-im.chat.models.message :as models.message] [status-im.transport.message.v1.protocol :as protocol] [status-im.transport.utils :as transport.utils])) @@ -44,7 +39,7 @@ (= (get-in cofx [:db :chats chat-id :group-admin]) (get-in cofx [:db :current-public-key]))) -(defn- send-new-group-key [message chat-id cofx] +(defn send-new-group-key [message chat-id cofx] (when (user-is-group-admin? chat-id cofx) {:shh/get-new-sym-keys [{:web3 (get-in cofx [:db :web3]) :on-success (fn [sym-key sym-key-id] @@ -55,83 +50,21 @@ :sym-key-id sym-key-id :message message}]))}]})) -(defn- prepare-system-message [admin-name added-participants removed-participants contacts] - (let [added-participants-names (map #(get-in contacts [% :name] %) added-participants) - removed-participants-names (map #(get-in contacts [% :name] %) removed-participants)] - (cond - (and (seq added-participants) (seq removed-participants)) - (str admin-name " " - (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names)) - " and " - (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names))) - - (seq added-participants) - (str admin-name " " (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names))) - - (seq removed-participants) - (str admin-name " " (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names)))))) - (defn- init-chat-if-new [chat-id cofx] (if (nil? (get-in cofx [:db :transport/chats chat-id])) (protocol/init-chat chat-id cofx))) -(defn- participants-diff [existing-participants-set new-participants-set] - {:removed (set/difference existing-participants-set new-participants-set) - :added (set/difference new-participants-set existing-participants-set)}) - (defrecord GroupAdminUpdate [chat-name participants] message/StatusMessage (send [this chat-id cofx] (handlers-macro/merge-fx cofx (init-chat-if-new chat-id) - (send-new-group-key this chat-id))) - (receive [this chat-id signature {:keys [now db random-id] :as cofx}] - (let [me (:current-public-key db)] - ;; we have to check if we already have a chat, or it's a new one - (if-let [{:keys [group-admin contacts] :as chat} (get-in db [:chats chat-id])] - ;; update for existing group chat - (when (and (= signature group-admin) ;; make sure that admin is the one making changes - (not= (set contacts) (set participants))) ;; make sure it's actually changing something - (let [{:keys [removed added]} (participants-diff (set contacts) (set participants)) - admin-name (or (get-in db [:contacts/contacts group-admin :name]) - group-admin)] - (if (removed me) ;; we were removed - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (str admin-name " " (i18n/label :t/removed-from-chat)))) - (models.chat/upsert-chat {:chat-id chat-id - :removed-from-at now - :is-active false}) - (transport.utils/unsubscribe-from-chat chat-id)) - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (prepare-system-message admin-name - added - removed - (:contacts/contacts db)))) - (group/participants-added chat-id added) - (group/participants-removed chat-id removed))))) - ;; first time we hear about chat -> create it if we are among participants - (when (get (set participants) me) - (models.chat/add-group-chat chat-id chat-name signature participants cofx)))))) + (send-new-group-key this chat-id)))) (defrecord GroupLeave [] message/StatusMessage (send [this chat-id cofx] (protocol/send {:chat-id chat-id :payload this - :success-event [::unsubscribe-from-chat chat-id]} - cofx)) - (receive [this chat-id signature {:keys [db now random-id] :as cofx}] - (let [me (:current-public-key db) - participant-leaving-name (or (get-in db [:contacts/contacts signature :name]) - signature)] - (when (get-in db [:chats chat-id]) ;; chat is present - (handlers-macro/merge-fx cofx - (models.message/receive - (models.message/system-message chat-id random-id now - (str participant-leaving-name " " (i18n/label :t/left)))) - (group/participants-removed chat-id #{signature}) - (send-new-group-key nil chat-id)))))) + :success-event [:group/unsubscribe-from-chat chat-id]} + cofx))) diff --git a/src/status_im/ui/screens/group/chat_settings/events.cljs b/src/status_im/ui/screens/group/chat_settings/events.cljs index 8bf9003771..221669999c 100644 --- a/src/status_im/ui/screens/group/chat_settings/events.cljs +++ b/src/status_im/ui/screens/group/chat_settings/events.cljs @@ -2,6 +2,7 @@ (:require [re-frame.core :as re-frame] [status-im.i18n :as i18n] [status-im.chat.models.message :as models.message] + [status-im.chat.models :as models.chat] [status-im.ui.screens.navigation :as navigation] [status-im.transport.message.v1.group-chat :as group-chat] [status-im.transport.message.core :as transport] @@ -57,19 +58,3 @@ {:db (assoc-in db [:chats current-chat-id :name] new-chat-name) :data-store/tx [(chats-store/save-chat-tx {:chat-id current-chat-id :name new-chat-name})]})) - -(handlers/register-handler-fx - :clear-history - (fn [{{:keys [current-chat-id] :as db} :db} _] - {:db (-> db - (assoc-in [:chats current-chat-id :messages] {}) - (assoc-in [:chats current-chat-id :message-groups] {})) - :data-store/tx [(messages-store/hide-messages-tx current-chat-id)]})) - -(handlers/register-handler-fx - :clear-history? - (fn [_ _] - {:show-confirmation {:title (i18n/label :t/clear-history-confirmation) - :content (i18n/label :t/clear-group-history-confirmation) - :confirm-button-text (i18n/label :t/clear) - :on-accept #(re-frame/dispatch [:clear-history])}})) diff --git a/src/status_im/ui/screens/profile/group_chat/views.cljs b/src/status_im/ui/screens/profile/group_chat/views.cljs index 42011ea3b6..9c2ed5f82a 100644 --- a/src/status_im/ui/screens/profile/group_chat/views.cljs +++ b/src/status_im/ui/screens/profile/group_chat/views.cljs @@ -44,25 +44,17 @@ [{:label (i18n/label :t/clear-history) :icon :icons/close :action #(utils/show-confirmation (i18n/label :t/clear-history-title) - (i18n/label :t/clear-group-history-confirmation) + (i18n/label :t/clear-history-confirmation-content) (i18n/label :t/clear-history-action) (fn [] (re-frame/dispatch [:clear-history]))) :accessibility-label :clear-history-button} {:label (i18n/label :t/delete-chat) - :icon :icons/delete - :action #(utils/show-confirmation (i18n/label :t/delete-chat-title) - (i18n/label :t/delete-group-chat-confirmation) - (i18n/label :t/delete) - (fn [] - (re-frame/dispatch [:remove-chat-and-navigate-home chat-id]))) - :accessibility-label :delete-chat-button} - {:label (i18n/label :t/leave-group) :icon :icons/arrow-left :action #(utils/show-confirmation (i18n/label :t/leave-group-title) (i18n/label :t/leave-group-confirmation) (i18n/label :t/leave-group-action) (fn [] (re-frame/dispatch [:remove-chat-and-navigate-home chat-id]))) - :accessibility-label :leave-chat-button}])) + :accessibility-label :delete-chat-button}])) (defn contact-actions [contact] [{:action #(re-frame/dispatch [:show-profile (:whisper-identity contact)]) diff --git a/test/cljs/status_im/test/chat/models.cljs b/test/cljs/status_im/test/chat/models.cljs index e196c6af16..84ddb03267 100644 --- a/test/cljs/status_im/test/chat/models.cljs +++ b/test/cljs/status_im/test/chat/models.cljs @@ -1,5 +1,6 @@ (ns status-im.test.chat.models (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.clocks :as utils.clocks] [status-im.chat.models :as chat])) (deftest upsert-chat-test @@ -100,3 +101,104 @@ (is (:group-chat chat))) (testing "it does not sets the public flag" (is (:public? chat))))) + +(deftest clear-history-test + (let [chat-id "1" + cofx {:db {:chats {chat-id {:message-groups {:something "a"} + :messages {"1" {:clock-value 1} + "2" {:clock-value 10} + "3" {:clock-value 2}}}}}}] + (testing "it deletes all the messages" + (let [actual (chat/clear-history chat-id cofx)] + (is (= {} (get-in actual [:db :chats chat-id :messages]))))) + (testing "it deletes all the message groups" + (let [actual (chat/clear-history chat-id cofx)] + (is (= {} (get-in actual [:db :chats chat-id :message-groups]))))) + (testing "it sets a deleted-at-clock-value equal to the last message clock-value" + (let [actual (chat/clear-history chat-id cofx)] + (is (= 10 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) + (testing "it does not override the deleted-at-clock-value when there are no messages" + (let [actual (chat/clear-history chat-id + (update-in cofx + [:db :chats chat-id] + assoc + :messages {} + :deleted-at-clock-value 100))] + (is (= 100 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) + (testing "it set the deleted-at-clock-value to now the chat has no messages nor previous deleted-at" + (with-redefs [utils.clocks/send (constantly 42)] + (let [actual (chat/clear-history chat-id + (update-in cofx + [:db :chats chat-id] + assoc + :messages {}))] + (is (= 42 (get-in actual [:db :chats chat-id :deleted-at-clock-value])))))) + (testing "it adds the relevant transactions for realm" + (let [actual (chat/clear-history chat-id cofx)] + (is (:data-store/tx actual)) + (is (= 2 (count (:data-store/tx actual)))))))) + +(deftest remove-chat-test + (let [chat-id "1" + cofx {:db {:transport/chats {chat-id {}} + :chats {chat-id {:messages {"1" {:clock-value 1} + "2" {:clock-value 10} + "3" {:clock-value 2}}}}}}] + (testing "it deletes all the messages" + (let [actual (chat/remove-chat chat-id cofx)] + (is (= {} (get-in actual [:db :chats chat-id :messages]))))) + (testing "it sets a deleted-at-clock-value equal to the last message clock-value" + (let [actual (chat/remove-chat chat-id cofx)] + (is (= 10 (get-in actual [:db :chats chat-id :deleted-at-clock-value]))))) + (testing "it sets the chat as inactive" + (let [actual (chat/remove-chat chat-id cofx)] + (is (= false (get-in actual [:db :chats chat-id :is-active]))))) + (testing "it removes it from transport if it's a public chat" + (let [actual (chat/remove-chat chat-id + (update-in + cofx + [:db :chats chat-id] + assoc + :group-chat true + :public? true))] + (is (not (get-in actual [:db :transport/chats chat-id]))))) + (testing "it sends a leave group request if it's a group-chat" + (let [actual (chat/remove-chat chat-id + (assoc-in + cofx + [:db :chats chat-id :group-chat] + true))] + (is (:shh/post actual)) + (testing "it does not remove transport, only after send is successful" + (is (get-in actual [:db :transport/chats chat-id]))))) + (testing "it does not remove it from transport if it's a one-to-one" + (let [actual (chat/remove-chat chat-id cofx)] + (is (get-in actual [:db :transport/chats chat-id])))) + (testing "it adds the relevant transactions for realm" + (let [actual (chat/remove-chat chat-id cofx)] + (is (:data-store/tx actual)) + (is (= 3 (count (:data-store/tx actual)))))))) + +(deftest multi-user-chat? + (let [chat-id "1"] + (testing "it returns true if it's a group chat" + (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] + (is (chat/multi-user-chat? chat-id cofx)))) + (testing "it returns true if it's a public chat" + (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] + (is (chat/multi-user-chat? chat-id cofx)))) + (testing "it returns false if it's a 1-to-1 chat" + (let [cofx {:db {:chats {chat-id {}}}}] + (is (not (chat/multi-user-chat? chat-id cofx))))))) + +(deftest group-chat? + (let [chat-id "1"] + (testing "it returns true if it's a group chat" + (let [cofx {:db {:chats {chat-id {:group-chat true}}}}] + (is (chat/group-chat? chat-id cofx)))) + (testing "it returns false if it's a public chat" + (let [cofx {:db {:chats {chat-id {:public? true :group-chat true}}}}] + (is (not (chat/group-chat? chat-id cofx))))) + (testing "it returns false if it's a 1-to-1 chat" + (let [cofx {:db {:chats {chat-id {}}}}] + (is (not (chat/group-chat? chat-id cofx))))))) diff --git a/test/cljs/status_im/test/chat/models/message.cljs b/test/cljs/status_im/test/chat/models/message.cljs index 592cc03823..74fe67f2fd 100644 --- a/test/cljs/status_im/test/chat/models/message.cljs +++ b/test/cljs/status_im/test/chat/models/message.cljs @@ -9,22 +9,44 @@ (is (message/add-to-chat? {:db {:chats {"a" {}}}} {:message-id "message-id" :from "a" + :clock-value 1 :chat-id "a"}))) (testing "it returns false when from is the same as pk" (is (not (message/add-to-chat? {:db {:current-public-key "pk" :chats {"a" {}}}} {:message-id "message-id" :from "pk" + :clock-value 1 :chat-id "a"})))) (testing "it returns false when it's already in the loaded message" (is (not (message/add-to-chat? {:db {:chats {"a" {:messages {"message-id" {}}}}}} {:message-id "message-id" :from "a" + :clock-value 1 :chat-id "a"})))) (testing "it returns false when it's already in the not-loaded-message-ids" (is (not (message/add-to-chat? {:db {:chats {"a" {:not-loaded-message-ids #{"message-id"}}}}} {:message-id "message-id" :from "a" + :clock-value 1 + :chat-id "a"})))) + (testing "it returns false when the clock-value is the same as the deleted-clock-value in chat" + (is (not (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} + {:message-id "message-id" + :from "a" + :clock-value 1 + :chat-id "a"})))) + (testing "it returns true when the clock-value is greater than the deleted-clock-value in chat" + (is (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} + {:message-id "message-id" + :from "a" + :clock-value 2 + :chat-id "a"}))) + (testing "it returns false when the clock-value is less than the deleted-clock-value in chat" + (is (not (message/add-to-chat? {:db {:chats {"a" {:deleted-at-clock-value 1}}}} + {:message-id "message-id" + :from "a" + :clock-value 0 :chat-id "a"}))))) (deftest receive-send-seen diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 454f54ce60..f872eb28bb 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -22,6 +22,7 @@ [status-im.test.protocol.web3.inbox] [status-im.test.utils.utils] [status-im.test.utils.money] + [status-im.test.utils.handlers-macro] [status-im.test.utils.clocks] [status-im.test.utils.inbox] [status-im.test.utils.ethereum.eip681] @@ -66,6 +67,7 @@ 'status-im.test.transport.inbox 'status-im.test.protocol.web3.inbox 'status-im.test.utils.utils + 'status-im.test.utils.handlers-macro 'status-im.test.utils.money 'status-im.test.utils.clocks 'status-im.test.utils.inbox diff --git a/test/cljs/status_im/test/utils/handlers_macro.cljs b/test/cljs/status_im/test/utils/handlers_macro.cljs new file mode 100644 index 0000000000..7340d35dfd --- /dev/null +++ b/test/cljs/status_im/test/utils/handlers_macro.cljs @@ -0,0 +1,32 @@ +(ns status-im.test.utils.handlers-macro + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.handlers-macro :as m])) + +(deftest merge-fx + (letfn [(add-b [cofx] + (assoc-in cofx [:db :b] "b")) + (add-c [cofx] + (assoc-in cofx [:db :c] "c")) + (add-tx [tx cofx] + (assoc cofx :data-store/tx [tx]))] + (testing "it updates db correctly" + (let [actual (m/merge-fx {:db {:a "a"}} + (add-b) + (add-c))] + (is (= {:db {:a "a" + :b "b" + :c "c"}} actual)))) + (testing "it updates db correctly when a fn don't update it" + (let [empty-fn (constantly nil) + actual (m/merge-fx {:db {:a "a"}} + (add-b) + (empty-fn) + (add-c))] + (is (= {:db {:a "a" + :b "b" + :c "c"}} actual)))) + #_(testing "it updates data-store/tx correctly" + (let [actual (m/merge-fx {:data-store/tx [1]} + (add-tx 2) + (add-tx 3))] + (is (= {:data-store/tx [1 2 3]} actual))))))