diff --git a/STATUS_GO_VERSION b/STATUS_GO_VERSION index 04a373efe6..2a0970ca75 100644 --- a/STATUS_GO_VERSION +++ b/STATUS_GO_VERSION @@ -1 +1 @@ -0.16.0 +0.16.1 diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index 0d05eb1f67..d050d84e0f 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -694,8 +694,8 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } @ReactMethod - public void verifyGroupMembershipSignatures(final String signaturePairs, final Callback callback) { - Log.d(TAG, "verifyGroupMembershipSignatures"); + public void extractGroupMembershipSignatures(final String signaturePairs, final Callback callback) { + Log.d(TAG, "extractGroupMembershipSignatures"); if (!checkAvailability()) { callback.invoke(false); return; @@ -704,7 +704,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL Runnable r = new Runnable() { @Override public void run() { - String result = Statusgo.VerifyGroupMembershipSignatures(signaturePairs); + String result = Statusgo.ExtractGroupMembershipSignatures(signaturePairs); callback.invoke(result); } diff --git a/modules/react-native-status/desktop/CMakeLists.txt b/modules/react-native-status/desktop/CMakeLists.txt index d5b741ef44..41c3c75d1d 100755 --- a/modules/react-native-status/desktop/CMakeLists.txt +++ b/modules/react-native-status/desktop/CMakeLists.txt @@ -37,7 +37,8 @@ ExternalProject_Add(StatusGo_ep PREFIX ${StatusGo_PREFIX} SOURCE_DIR ${StatusGo_SOURCE_DIR} GIT_REPOSITORY https://github.com/status-im/status-go.git - GIT_TAG f3880f8fe1f11e2cd59382c34dd826ebbf9662cf + GIT_TAG 9f8f0089a3561e77b25279575928de8caba373cc + BUILD_BYPRODUCTS ${StatusGo_STATIC_LIB} CONFIGURE_COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/${CONFIGURE_SCRIPT} ${GO_ROOT_PATH} ${StatusGo_ROOT} ${StatusGo_SOURCE_DIR} BUILD_COMMAND "" diff --git a/modules/react-native-status/desktop/rctstatus.cpp b/modules/react-native-status/desktop/rctstatus.cpp index f975a99292..4fa6e1ae59 100644 --- a/modules/react-native-status/desktop/rctstatus.cpp +++ b/modules/react-native-status/desktop/rctstatus.cpp @@ -203,12 +203,12 @@ void RCTStatus::signGroupMembership(QString content, double callbackId) { }, content, callbackId); } -void RCTStatus::verifyGroupMembershipSignatures(QString signatures, double callbackId) { +void RCTStatus::extractGroupMembershipSignatures(QString signatures, double callbackId) { Q_D(RCTStatus); - qDebug() << "call of RCTStatus::verifyGroupMembershipSignatures with param callbackId: " << callbackId; + qDebug() << "call of RCTStatus::extractGroupMembershipSignatures with param callbackId: " << callbackId; QtConcurrent::run([&](QString signatures, double callbackId) { - const char* result = VerifyGroupMembershipSignatures(signatures.toUtf8().data()); - qDebug() << "RCTStatus::verifyGroupMembershipSignatures VerifyGroupMembershipSignatures result: " << statusGoResultError(result); + const char* result = ExtractGroupMembershipSignatures(signatures.toUtf8().data()); + qDebug() << "RCTStatus::extractGroupMembershipSignatures ExtractGroupMembershipSignatures result: " << statusGoResultError(result); d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); }, signatures, callbackId); } diff --git a/modules/react-native-status/desktop/rctstatus.h b/modules/react-native-status/desktop/rctstatus.h index 21e2b40eda..6ffad6cb40 100644 --- a/modules/react-native-status/desktop/rctstatus.h +++ b/modules/react-native-status/desktop/rctstatus.h @@ -42,7 +42,7 @@ public: Q_INVOKABLE void sendTransaction(QString txArgsJSON, QString password, double callbackId); Q_INVOKABLE void signMessage(QString rpcParams, double callbackId); Q_INVOKABLE void signGroupMembership(QString content, double callbackId); - Q_INVOKABLE void verifyGroupMembershipSignatures(QString signatures, double callbackId); + Q_INVOKABLE void extractGroupMembershipSignatures(QString signatures, double callbackId); Q_INVOKABLE void setAdjustResize(); Q_INVOKABLE void setAdjustPan(); diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index a7c6203e3f..ae94caffd4 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -266,14 +266,14 @@ RCT_EXPORT_METHOD(signGroupMembership:(NSString *)content } //////////////////////////////////////////////////////////////////// -#pragma mark - VerifyGroupMembershipSignatures -//////////////////////////////////////////////////////////////////// verifyGroupMembershipSignatures -RCT_EXPORT_METHOD(verifyGroupMembershipSignatures:(NSString *)content +#pragma mark - ExtractGroupMembershipSignatures +//////////////////////////////////////////////////////////////////// extractGroupMembershipSignatures +RCT_EXPORT_METHOD(extractGroupMembershipSignatures:(NSString *)content callback:(RCTResponseSenderBlock)callback) { #if DEBUG - NSLog(@"VerifyGroupMembershipSignatures() method called"); + NSLog(@"ExtractGroupMembershipSignatures() method called"); #endif - char * result = VerifyGroupMembershipSignatures((char *) [content UTF8String]); + char * result = ExtractGroupMembershipSignatures((char *) [content UTF8String]); callback(@[[NSString stringWithUTF8String: result]]); } diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index 2956afd209..21d99a7b9d 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -25,7 +25,7 @@ (and (multi-user-chat? cofx chat-id) (not (get-in cofx [:db :chats chat-id :public?])))) -(defn public-chat? [chat-id cofx] +(defn public-chat? [cofx chat-id] (get-in cofx [:db :chats chat-id :public?])) (defn set-chat-ui-props @@ -47,7 +47,7 @@ :group-chat false :is-active true :timestamp now - :contacts [chat-id] + :contacts #{chat-id} :last-clock-value 0})) (fx/defn upsert-chat @@ -58,11 +58,8 @@ (create-new-chat chat-id cofx)) chat-props)] - (if (:is-active chat) - {:db (update-in db [:chats chat-id] merge chat) - :data-store/tx [(chats-store/save-chat-tx chat)]} - ;; when chat is deleted, don't change anything - {:db db}))) + {:db (update-in db [:chats chat-id] merge chat) + :data-store/tx [(chats-store/save-chat-tx chat)]})) (fx/defn add-public-chat "Adds new public group chat to db & realm" @@ -72,7 +69,7 @@ :is-active true :name topic :group-chat true - :contacts [] + :contacts #{} :public? true})) (fx/defn add-group-chat @@ -108,11 +105,8 @@ (messages-store/delete-messages-tx chat-id)]})) (fx/defn remove-transport - [{:keys [db] :as cofx} chat-id] - ;; if this is private group chat, we have to broadcast leave and unsubscribe after that - (if (group-chat? cofx chat-id) - (transport.message/send (transport/GroupLeave.) chat-id cofx) - (transport.utils/unsubscribe-from-chat cofx chat-id))) + [cofx chat-id] + (transport.utils/unsubscribe-from-chat cofx chat-id)) (fx/defn deactivate-chat [{:keys [db now] :as cofx} chat-id] @@ -131,7 +125,7 @@ "Removes chat completely from app, producing all necessary effects for that" [{:keys [db now] :as cofx} chat-id] (fx/merge cofx - #(when (multi-user-chat? % chat-id) + #(when (public-chat? % chat-id) (remove-transport % chat-id)) (deactivate-chat chat-id) (clear-history chat-id) diff --git a/src/status_im/chat/models/group_chat.cljs b/src/status_im/chat/models/group_chat.cljs deleted file mode 100644 index 312316c4f7..0000000000 --- a/src/status_im/chat/models/group_chat.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.chat.models.group-chat - (:require [clojure.set :as set] - [clojure.string :as string] - [status-im.i18n :as i18n] - [status-im.transport.utils :as transport.utils] - [status-im.transport.message.core :as transport] - [status-im.transport.message.v1.core :as transport.message] - [status-im.ui.screens.group.core :as group] - [status-im.group-chats.core :as group-chat] - [status-im.chat.models :as models.chat] - [status-im.transport.message.core :as message] - [status-im.chat.models.message :as models.message] - [status-im.utils.fx :as fx])) - -(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)))))) - -(fx/defn handle-group-leave - [{:keys [db random-id-generator now] :as cofx} chat-id signature] - (let [me (:current-public-key db) - system-message-id (random-id-generator) - 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 - - (fx/merge 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}))))) - -(defn- group-name-from-contacts [selected-contacts all-contacts username] - (->> selected-contacts - (map (comp :name (partial get all-contacts))) - (cons username) - (string/join ", "))) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index 160497060d..d96f7ecbcf 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -51,7 +51,7 @@ chat/cooldowns chat/spam-messages-frequency current-chat-id] :as db} :db :as cofx}] - (when (chat/public-chat? current-chat-id cofx) + (when (chat/public-chat? cofx current-chat-id) (let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at) (+ chat.constants/spam-interval-ms (* 1000 cooldowns))) spamming-frequently? (= chat.constants/spam-message-frequency-threshold spam-messages-frequency)] diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index 95c1b21578..49bf7a23cd 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -2,16 +2,61 @@ (:require [goog.object :as object] [cljs.core.async :as async] [re-frame.core :as re-frame] + [status-im.utils.ethereum.core :as utils.ethereum] [status-im.data-store.realm.core :as core])) +(defn remove-empty-vals + "Remove key/value when empty seq or nil" + [e] + (into {} (remove (fn [[_ v]] + (or (nil? v) + (and (coll? v) + (empty? v)))) e))) + +(defn- event->string + "Transform an event in an a vector with keys in alphabetical order, to compute + a predictable id" + [event] + (js/JSON.stringify + (clj->js + (mapv + #(vector % (get event %)) + (sort (keys event)))))) + +; Build an event id from a message +(def event-id (comp utils.ethereum/sha3 event->string)) + +(defn marshal-membership-updates [updates] + (mapcat (fn [{:keys [signature events from]}] + (map #(assoc % + :id (event-id %) + :signature signature + :from from) events)) updates)) + +(defn unmarshal-membership-updates [chat-id updates] + (->> updates + vals + (group-by :signature) + (map (fn [[signature events]] + {:events (map #(-> (dissoc % :signature :from :id) + remove-empty-vals) events) + :from (-> events first :from) + :signature signature + :chat-id chat-id})))) + +(defn- get-last-clock-value [chat-id] + (-> (core/get-by-field @core/account-realm + :message :chat-id chat-id) + (core/sorted :clock-value :desc) + (core/single-clj :message) + :clock-value)) + (defn- normalize-chat [{:keys [chat-id] :as chat}] - (let [last-clock-value (-> (core/get-by-field @core/account-realm - :message :chat-id chat-id) - (core/sorted :clock-value :desc) - (core/single-clj :message) - :clock-value)] + (let [last-clock-value (get-last-clock-value chat-id)] (-> chat + (update :admins #(into #{} %)) (update :contacts #(into #{} %)) + (update :membership-updates (partial unmarshal-membership-updates chat-id)) (assoc :last-clock-value (or last-clock-value 0))))) (re-frame/reg-cofx @@ -27,7 +72,11 @@ "Returns tx function for saving chat" [{:keys [chat-id] :as chat}] (fn [realm] - (core/create realm :chat chat true))) + (core/create + realm + :chat + (update chat :membership-updates marshal-membership-updates) + true))) ;; Only used in debug mode (defn delete-chat-tx diff --git a/src/status_im/data_store/realm/schemas/account/chat.cljs b/src/status_im/data_store/realm/schemas/account/chat.cljs index 08a6795b2f..5ff52a565e 100644 --- a/src/status_im/data_store/realm/schemas/account/chat.cljs +++ b/src/status_im/data_store/realm/schemas/account/chat.cljs @@ -126,3 +126,34 @@ :default false} :public? {:type :bool :default false}}}) + +(def v7 {:name :chat + :primaryKey :chat-id + :properties {:chat-id :string + :name :string + :color {:type :string + :default default-chat-color} + :group-chat {:type :bool + :indexed true} + :is-active :bool + :timestamp :int + :contacts {:type "string[]"} + :admins {:type "string[]"} + :membership-updates {:type :list + :objectType :membership-update} + :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} + :debug? {:type :bool + :default false} + :public? {:type :bool + :default false}}}) 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 8ae8f345f2..83fcfe5e24 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -9,6 +9,7 @@ [status-im.data-store.realm.schemas.account.browser :as browser] [status-im.data-store.realm.schemas.account.dapp-permissions :as dapp-permissions] [status-im.data-store.realm.schemas.account.request :as request] + [status-im.data-store.realm.schemas.account.membership-update :as membership-update] [status-im.data-store.realm.schemas.account.migrations :as migrations] [taoensso.timbre :as log])) @@ -152,6 +153,17 @@ browser/v8 dapp-permissions/v9]) +(def v15 [chat/v7 + transport/v6 + contact/v1 + message/v7 + mailserver/v11 + user-status/v1 + membership-update/v1 + local-storage/v1 + browser/v8 + dapp-permissions/v9]) + ;; put schemas ordered by version (def schemas [{:schema v1 :schemaVersion 1 diff --git a/src/status_im/data_store/realm/schemas/account/membership_update.cljs b/src/status_im/data_store/realm/schemas/account/membership_update.cljs new file mode 100644 index 0000000000..0867664b88 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/membership_update.cljs @@ -0,0 +1,15 @@ +(ns status-im.data-store.realm.schemas.account.membership-update) + +(def v1 {:name :membership-update + :primaryKey :id + :properties {:id :string + :type :string + :name {:type :string + :optional true} + :clock-value :int + :signature :string + :from :string + :member {:type :string + :optional true} + :members {:type "string[]" + :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/migrations.cljs b/src/status_im/data_store/realm/schemas/account/migrations.cljs index 36299fb777..800d32d2cb 100644 --- a/src/status_im/data_store/realm/schemas/account/migrations.cljs +++ b/src/status_im/data_store/realm/schemas/account/migrations.cljs @@ -94,3 +94,6 @@ (.filtered (str "content-type = \"command-request\"")) (.map (fn [message _ _] (aset message "content-type" "command"))))) + +(defn v15 [old-realm new-realm] + (log/debug "migrating v13 account database")) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index abeb5da1af..2f3be56123 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -10,7 +10,6 @@ [status-im.browser.core :as browser] [status-im.browser.permissions :as browser.permissions] [status-im.chat.models :as chat] - [status-im.chat.models.group-chat :as chat.group] [status-im.chat.models.message :as chat.message] [status-im.chat.models.loading :as chat.loading] [status-im.chat.models.input :as chat.input] @@ -470,7 +469,7 @@ (handlers/register-handler-fx :chat.ui/remove-chat-pressed - (fn [_ [_ chat-id group?]] + (fn [_ [_ chat-id]] {:ui/show-confirmation {:title (i18n/label :t/delete-confirmation) :content (i18n/label :t/delete-chat-confirmation) :confirm-button-text (i18n/label :t/delete) @@ -903,6 +902,29 @@ (fn [cofx _] (group-chats/save cofx))) +(handlers/register-handler-fx + :group-chats.ui/add-members-pressed + (fn [cofx _] + (group-chats/add-members cofx))) + +(handlers/register-handler-fx + :group-chats.ui/remove-member-pressed + (fn [cofx [_ chat-id public-key]] + (group-chats/remove-member cofx chat-id public-key))) + +(handlers/register-handler-fx + :group-chats.ui/remove-chat-pressed + (fn [_ [_ chat-id group?]] + {:ui/show-confirmation {:title (i18n/label :t/delete-confirmation) + :content (i18n/label :t/delete-chat-confirmation) + :confirm-button-text (i18n/label :t/delete) + :on-accept #(re-frame/dispatch [:group-chats.ui/remove-chat-confirmed chat-id])}})) + +(handlers/register-handler-fx + :group-chats.ui/remove-chat-confirmed + (fn [cofx [_ chat-id]] + (group-chats/remove cofx chat-id))) + (handlers/register-handler-fx :group-chats.callback/sign-success [(re-frame/inject-cofx :random-guid-generator)] @@ -910,6 +932,6 @@ (group-chats/handle-sign-success cofx group-update))) (handlers/register-handler-fx - :group-chats.callback/verify-signature-success + :group-chats.callback/extract-signature-success (fn [cofx [_ group-update sender-signature]] (group-chats/handle-membership-update cofx group-update sender-signature))) diff --git a/src/status_im/group_chats/core.cljs b/src/status_im/group_chats/core.cljs index cc576c2e53..7c34c16483 100644 --- a/src/status_im/group_chats/core.cljs +++ b/src/status_im/group_chats/core.cljs @@ -1,8 +1,12 @@ (ns status-im.group-chats.core + (:refer-clojure :exclude [remove]) (:require [clojure.string :as string] [clojure.spec.alpha :as spec] + [clojure.set :as clojure.set] [re-frame.core :as re-frame] + [status-im.i18n :as i18n] [status-im.utils.config :as config] + [status-im.utils.clocks :as utils.clocks] [status-im.native-module.core :as native-module] [status-im.transport.utils :as transport.utils] [status-im.transport.db :as transport.db] @@ -13,21 +17,54 @@ [status-im.utils.fx :as fx] [status-im.chat.models :as models.chat])) +;; Description of the flow: +;; the flow is complicated a bit by 2 asynchronous call to status-go, which might make the logic a bit more opaque. +;; To send a group-membership update, we first build a message. +;; We then sign it with our private key and dispatch it. +;; Conversely when receiving a message, we first extract the public keys from the signature and attach those in the :from field. +;; We then process the events. +;; It is importatn that the from field is not trusted for updates coming from the outside. +;; When messages are coming from the database it can be trustured as already verified by us. + + +(defn sort-events [events] + (sort-by :clock-value events)) + +(defn- event->vector + "Transform an event in an a vector with keys in alphabetical order" + [event] + (mapv + #(vector % (get event %)) + (sort (keys event)))) + +(defn get-last-clock-value + "Given a chat id get the last clock value of an event" + [cofx chat-id] + (->> (get-in cofx [:db :chats chat-id :membership-updates]) + (mapcat :events) + (map :clock-value) + sort + last)) + (defn- parse-response [response-js] (-> response-js js/JSON.parse (js->clj :keywordize-keys true))) -(defn signature-material [{:keys [chat-id admin participants]}] - (apply str - (concat (sort participants) - admin - chat-id))) +(defn signature-material + "Transform an update into a signable string" + [chat-id events] + (js/JSON.stringify + (clj->js [(mapv event->vector (sort-events events)) chat-id]))) -(defn signature-pairs [{:keys [admin signature] :as payload}] - (js/JSON.stringify (clj->js [[(signature-material payload) - signature - (subs admin 2)]]))) +(defn signature-pairs + "Transform a bunch of updates into signable pairs to be verified" + [{:keys [chat-id membership-updates] :as payload}] + (let [pairs (mapv (fn [{:keys [events signature]}] + [(signature-material chat-id events) + signature]) + membership-updates)] + (js/JSON.stringify (clj->js pairs)))) (defn valid-chat-id? ;; We need to make sure the chat-id ends with the admin pk (and it's not the same). @@ -39,103 +76,69 @@ (and (string/ends-with? chat-id admin) (not= chat-id admin))) +(defn valid-event? + "Check if event can be applied to current group" + [{:keys [admins contacts]} {:keys [chat-id from member members] :as new-event}] + (when from + (condp = (:type new-event) + "chat-created" (and (empty? admins) + (empty? contacts)) + "name-changed" (and (admins from) + (not (string/blank? (:name new-event)))) + "members-added" (admins from) + "admins-added" (and (admins from) + (clojure.set/subset? members contacts)) + "member-removed" (or + ;; An admin removing a member + (and (admins from) + (not (admins member))) + ;; Members can remove themselves + (and (not (admins member)) + (contacts member) + (= from member))) + "admin-removed" (and (admins from) + (= from member) + (not= #{from} admins)) + false))) + (defn wrap-group-message "Wrap a group message in a membership update" [cofx chat-id message] (when-let [chat (get-in cofx [:db :chats chat-id])] (transport/map->GroupMembershipUpdate. - {:chat-id chat-id - :chat-name (:name chat) - :admin (:group-admin chat) - :participants (:contacts chat) - :signature (:membership-signature chat) - :version (:membership-version chat) - :message message}))) - -(fx/defn update-membership - "Upsert chat when version is greater or not existing" - [cofx previous-chat {:keys [chat-id - chat-name - participants - leaves - admin - signature - version]}] - (when - (or - (nil? previous-chat) - (< (:membership-version previous-chat) - version)) - - (models.chat/upsert-chat cofx - {:chat-id chat-id - :name chat-name - :is-active (get previous-chat :is-active true) - :group-chat true - :group-admin admin - :contacts participants - :membership-signature signature - :membership-version version}))) + {:chat-id chat-id + :membership-updates (:membership-updates chat) + :message message}))) (defn send-membership-update "Send a membership update to all participants but the sender" - [cofx payload chat-id] - (let [{:keys [participants]} payload - {:keys [current-public-key web3]} (:db cofx)] - (fx/merge - cofx - {:shh/send-group-message {:web3 web3 - :src current-public-key - :dsts (disj participants current-public-key) - :success-event [:transport/set-message-envelope-hash - chat-id - (transport.utils/message-id (:message payload)) - :group-user-message] - :payload payload}}))) - -(defn send-group-leave [payload chat-id cofx] - (transport.protocol/send cofx - {:chat-id chat-id - :payload payload - :success-event [:group/unsubscribe-from-chat chat-id]})) + ([cofx payload chat-id] + (send-membership-update cofx payload chat-id nil)) + ([cofx payload chat-id removed-members] + (let [members (clojure.set/union (get-in cofx [:db :chats chat-id :contacts]) + removed-members) + {:keys [current-public-key web3]} (:db cofx)] + (fx/merge + cofx + {:shh/send-group-message {:web3 web3 + :src current-public-key + :dsts (disj members current-public-key) + :success-event [:transport/set-message-envelope-hash + chat-id + (transport.utils/message-id (:message payload)) + :group-user-message] + :payload payload}})))) (fx/defn handle-membership-update-received - "Verify signatures in status-go and act if successful" + "Extract signatures in status-go and act if successful" [cofx membership-update signature] - {:group-chats/verify-membership-signature [[membership-update signature]]}) + {:group-chats/extract-membership-signature [[membership-update signature]]}) -(fx/defn handle-membership-update - "Upsert chat and receive message if valid" - ;; Care needs to be taken here as chat-id is not coming from a whisper filter - ;; so can be manipulated by the sending user. - [cofx {:keys [chat-id - chat-name - participants - signature - leaves - message - admin - version] :as membership-update} - sender-signature] - (when (and config/group-chats-enabled? - (valid-chat-id? chat-id admin)) - (let [previous-chat (get-in cofx [:db :chats chat-id])] - (fx/merge cofx - (update-membership previous-chat membership-update) - #(when (and message - ;; don't allow anything but group messages - (instance? transport.protocol/Message message) - (= :group-user-message (:message-type message))) - (protocol.message/receive message chat-id sender-signature nil %)))))) - -(defn handle-sign-success - "Upsert chat and send signed payload to group members" - [{:keys [db] :as cofx} {:keys [chat-id] :as group-update}] - (let [my-public-key (:current-public-key db)] - (fx/merge cofx - (models.chat/navigate-to-chat chat-id {:navigation-reset? true}) - (handle-membership-update group-update my-public-key) - #(protocol.message/send group-update chat-id %)))) +(defn chat->group-update + "Transform a chat in a GroupMembershipUpdate" + [chat-id {:keys [membership-updates]}] + (transport/map->GroupMembershipUpdate. {:chat-id chat-id + :membership-updates membership-updates})) (defn handle-sign-response "Callback to dispatch on sign response" @@ -145,39 +148,87 @@ (re-frame/dispatch [:group-chats.callback/sign-failed error]) (re-frame/dispatch [:group-chats.callback/sign-success (assoc payload :signature signature)])))) -(defn handle-verify-signature-response - "Callback to dispatch on verify signature response" - [payload sender-signature response-js] - (let [{:keys [error]} (parse-response response-js)] - (if error - (re-frame/dispatch [:group-chats.callback/verify-signature-failed error]) - (re-frame/dispatch [:group-chats.callback/verify-signature-success payload sender-signature])))) +(defn add-identities + "Add verified identities extracted from the signature to the updates" + [payload identities] + (update payload :membership-updates (fn [updates] + (map + #(assoc %1 :from (str "0x" %2)) + updates + identities)))) -(defn sign-membership [payload] - (native-module/sign-group-membership (signature-material payload) +(defn handle-extract-signature-response + "Callback to dispatch on extract signature response" + [payload sender-signature response-js] + (let [{:keys [error identities]} (parse-response response-js)] + (if error + (re-frame/dispatch [:group-chats.callback/extract-signature-failed error]) + (re-frame/dispatch [:group-chats.callback/extract-signature-success (add-identities payload identities) sender-signature])))) + +(defn sign-membership [{:keys [chat-id events] :as payload}] + (native-module/sign-group-membership (signature-material chat-id events) (partial handle-sign-response payload))) -(defn verify-membership-signature [signatures] - (doseq [[payload sender-signature] signatures] - (native-module/verify-group-membership-signatures (signature-pairs payload) - (partial handle-verify-signature-response payload sender-signature)))) +(defn extract-membership-signature [payload sender] + (native-module/extract-group-membership-signatures (signature-pairs payload) + (partial handle-extract-signature-response payload sender))) + +(defn- members-added-event [last-clock-value members] + {:type "members-added" + :clock-value (utils.clocks/send last-clock-value) + :members members}) (fx/defn create "Format group update message and sign membership" [{:keys [db random-guid-generator] :as cofx} group-name] (let [my-public-key (:current-public-key db) chat-id (str (random-guid-generator) my-public-key) - selected-contacts (conj (:group/selected-contacts db) - my-public-key) - group-update (transport/map->GroupMembershipUpdate - {:chat-id chat-id - :chat-name group-name - :admin my-public-key - :participants selected-contacts - :version 1})] - {:group-chats/sign-membership group-update + selected-contacts (:group/selected-contacts db) + clock-value (utils.clocks/send 0) + create-event {:type "chat-created" + :name group-name + :clock-value clock-value} + events [create-event + (members-added-event clock-value selected-contacts)]] + + {:group-chats/sign-membership {:chat-id chat-id + :from my-public-key + :events events} :db (assoc db :group/selected-contacts #{})})) +(fx/defn remove-member + "Format group update message and sign membership" + [{:keys [db] :as cofx} chat-id member] + (let [my-public-key (:current-public-key db) + last-clock-value (get-last-clock-value cofx chat-id) + chat (get-in cofx [:db :chats chat-id]) + remove-event {:type "member-removed" + :member member + :clock-value (utils.clocks/send last-clock-value)}] + (when (valid-event? chat (assoc remove-event + :from + my-public-key)) + {:group-chats/sign-membership {:chat-id chat-id + :from my-public-key + :events [remove-event]}}))) + +(fx/defn add-members + "Add members to a group chat" + [{{:keys [current-chat-id selected-participants current-public-key]} :db :as cofx}] + (let [last-clock-value (get-last-clock-value cofx current-chat-id) + events [(members-added-event last-clock-value selected-participants)]] + + {:group-chats/sign-membership {:chat-id current-chat-id + :from current-public-key + :events events}})) +(fx/defn remove + "Remove & leave chat" + [{:keys [db] :as cofx} chat-id] + (let [my-public-key (:current-public-key db)] + (fx/merge cofx + (remove-member chat-id my-public-key) + (models.chat/remove-chat chat-id)))) + (defn- valid-name? [name] (spec/valid? :profile/name name)) @@ -201,11 +252,94 @@ {:db (assoc db :group-chat-profile/editing? false)} (models.chat/upsert-chat {:chat-id current-chat-id :name new-name}))))) + +(defn process-event + "Add/remove an event to a group" + [group {:keys [type member members chat-id from name] :as event}] + (if (valid-event? group event) + (case type + "chat-created" {:name name + :admins #{from} + :contacts #{from}} + "name-changed" (assoc group :name name) + "members-added" (update group :contacts clojure.set/union (into #{} members)) + "admins-added" (update group :admins clojure.set/union (into #{} members)) + "member-removed" (update group :contacts disj member) + "admin-removed" (update group :admins disj member)) + group)) + +(defn build-group + "Given a list of already authenticated events build a group with contats/admin" + [events] + (->> events + sort-events + (reduce + process-event + {:admins #{} + :contacts #{}}))) + +(fx/defn update-membership + "Upsert chat when version is greater or not existing" + [cofx previous-chat {:keys [chat-id] :as new-chat}] + (let [all-updates (clojure.set/union (into #{} (:membership-updates previous-chat)) + (into #{} (:membership-updates new-chat))) + unwrapped-events (mapcat + (fn [{:keys [events from]}] + (map #(assoc % :from from) events)) + all-updates) + new-group (build-group unwrapped-events)] + (models.chat/upsert-chat cofx + {:chat-id chat-id + :name (:name new-group) + :is-active (get previous-chat :is-active true) + :group-chat true + :membership-updates (into [] all-updates) + :admins (:admins new-group) + :contacts (:contacts new-group)}))) + +(fx/defn handle-membership-update + "Upsert chat and receive message if valid" + ;; Care needs to be taken here as chat-id is not coming from a whisper filter + ;; so can be manipulated by the sending user. + [cofx {:keys [chat-id + message + membership-updates] :as membership-update} + sender-signature] + (when (and config/group-chats-enabled? + (valid-chat-id? chat-id (-> membership-updates first :from))) + (let [previous-chat (get-in cofx [:db :chats chat-id])] + (fx/merge cofx + (update-membership previous-chat membership-update) + #(when (and message + ;; don't allow anything but group messages + (instance? transport.protocol/Message message) + (= :group-user-message (:message-type message))) + (protocol.message/receive message chat-id sender-signature nil %)))))) + +(defn handle-sign-success + "Upsert chat and send signed payload to group members" + [{:keys [db] :as cofx} {:keys [chat-id] :as signed-events}] + (let [old-chat (get-in db [:chats chat-id]) + updated-chat (update old-chat :membership-updates conj signed-events) + my-public-key (:current-public-key db) + group-update (chat->group-update chat-id updated-chat) + new-group-fx (handle-membership-update group-update my-public-key) + ;; We need to send to users who have been removed as well + recipients (clojure.set/union + (:contacts old-chat) + (get-in new-group-fx [:db :chats chat-id :contacts]))] + (fx/merge cofx + new-group-fx + #(when (get-in % [:db :chats chat-id :is-active]) + (models.chat/navigate-to-chat % chat-id {:navigation-reset? true})) + #(send-membership-update % group-update chat-id recipients)))) + (re-frame/reg-fx :group-chats/sign-membership sign-membership) (re-frame/reg-fx - :group-chats/verify-membership-signature + :group-chats/extract-membership-signature (fn [signatures] - (verify-membership-signature signatures))) + (doseq [[payload sender] signatures] + (extract-membership-signature payload sender)))) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 8305b565e0..e001f2b42f 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -60,6 +60,6 @@ (defn is24Hour [] (native-module/is24Hour)) -(def verify-group-membership-signatures native-module/verify-group-membership-signatures) +(def extract-group-membership-signatures native-module/extract-group-membership-signatures) (def sign-group-membership native-module/sign-group-membership) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs index 4c9365d56e..0a8394e31f 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -134,9 +134,9 @@ (fn [UUID] (callback (string/upper-case UUID)))))) -(defn verify-group-membership-signatures [signature-pairs callback] +(defn extract-group-membership-signatures [signature-pairs callback] (when status - (call-module #(.verifyGroupMembershipSignatures status signature-pairs callback)))) + (call-module #(.extractGroupMembershipSignatures status signature-pairs callback)))) (defn sign-group-membership [content callback] (when status diff --git a/src/status_im/transport/db.cljs b/src/status_im/transport/db.cljs index 6f3aaa7ee3..4b7bd6cb15 100644 --- a/src/status_im/transport/db.cljs +++ b/src/status_im/transport/db.cljs @@ -43,8 +43,15 @@ (spec/def :chat/name (spec/nilable string?)) (spec/def :group-chat/admin :global/public-key) -(spec/def :group-chat/participants (spec/coll-of :global/public-key :kind set?)) -(spec/def :group-chat/signature string?) +(spec/def :group-chat/signature :global/not-empty-string) +(spec/def :group-chat/chat-id :global/not-empty-string) +(spec/def :group-chat/type :global/not-empty-string) +(spec/def :group-chat/member :global/not-empty-string) +(spec/def :group-chat/name :global/not-empty-string) + +(spec/def :group-chat/event (spec/keys :req-un [::clock-value :group-chat/type] :opt-un [:group-chat/member :group-chat/name])) +(spec/def :group-chat/events (spec/coll-of :group-chat/event)) +(spec/def :group-chat/membership-updates (spec/coll-of (spec/keys :req-un [:group-chat/signature :group-chat/events]))) (spec/def :message.content/text (spec/and string? (complement s/blank?))) (spec/def :message.content/response-to string?) @@ -73,7 +80,7 @@ (spec/def :message/message-seen (spec/keys :req-un [:message/ids])) -(spec/def :message/group-membership-update (spec/keys :req-un [:chat/chat-id :chat/name :group-chat/admin :group-chat/participants :group-chat/signature :message/message])) +(spec/def :message/group-membership-update (spec/keys :req-un [:group-chat/membership-updates :group-chat/chat-id])) (spec/def :message/message-common (spec/keys :req-un [::content-type ::message-type ::clock-value ::timestamp])) (spec/def :message.text/content (spec/keys :req-un [:message.content/text] diff --git a/src/status_im/transport/impl/receive.cljs b/src/status_im/transport/impl/receive.cljs index 759cad41e0..c073a06bdf 100644 --- a/src/status_im/transport/impl/receive.cljs +++ b/src/status_im/transport/impl/receive.cljs @@ -1,6 +1,5 @@ (ns status-im.transport.impl.receive (:require - [status-im.chat.models.group-chat :as models.group-chat] [status-im.models.contact :as models.contact] [status-im.group-chats.core :as group-chats] [status-im.transport.message.core :as message] @@ -12,11 +11,6 @@ (receive [this _ signature _ cofx] (group-chats/handle-membership-update-received cofx this signature))) -(extend-type transport.protocol/GroupLeave - message/StatusMessage - (receive [this chat-id signature _ cofx] - (models.group-chat/handle-group-leave cofx chat-id signature))) - (extend-type transport.contact/ContactRequest message/StatusMessage (receive [this _ signature timestamp cofx] diff --git a/src/status_im/transport/impl/send.cljs b/src/status_im/transport/impl/send.cljs index 8545bac278..c3dea46529 100644 --- a/src/status_im/transport/impl/send.cljs +++ b/src/status_im/transport/impl/send.cljs @@ -8,8 +8,3 @@ message/StatusMessage (send [this chat-id cofx] (group-chats/send-membership-update cofx this chat-id))) - -(extend-type transport/GroupLeave - message/StatusMessage - (send [this chat-id cofx] - (group-chats/send-group-leave this chat-id cofx))) diff --git a/src/status_im/transport/message/transit.cljs b/src/status_im/transport/message/transit.cljs index 6dd69e47ba..ef232d25fe 100644 --- a/src/status_im/transport/message/transit.cljs +++ b/src/status_im/transport/message/transit.cljs @@ -81,17 +81,11 @@ (rep [this {:keys [message-ids]}] (clj->js message-ids))) -(deftype GroupLeaveHandler [] - Object - (tag [this v] "g3") - (rep [this _] - (clj->js nil))) - (deftype GroupMembershipUpdateHandler [] Object (tag [this v] "g5") - (rep [this {:keys [chat-id chat-name admin participants leaves version signature message]}] - #js [chat-id chat-name admin participants leaves version signature message])) + (rep [this {:keys [chat-id membership-updates message]}] + #js [chat-id membership-updates message])) (def writer (transit/writer :json {:handlers @@ -101,7 +95,6 @@ v1.contact/ContactUpdate (ContactUpdateHandler.) v1.protocol/Message (MessageHandler.) v1.protocol/MessagesSeen (MessagesSeenHandler.) - v1/GroupLeave (GroupLeaveHandler.) v1/GroupMembershipUpdate (GroupMembershipUpdateHandler.)}})) ;; @@ -152,8 +145,8 @@ (v1.protocol/MessagesSeen. message-ids)) "c6" (fn [[name profile-image address fcm-token]] (v1.contact/ContactUpdate. name profile-image address fcm-token)) - "g5" (fn [[chat-id chat-name admin participants leaves version signature message]] - (v1/GroupMembershipUpdate. chat-id chat-name admin participants leaves version signature message))}})) ; removed group chat handlers for https://github.com/status-im/status-react/issues/4506 + "g5" (fn [[chat-id membership-updates message]] + (v1/GroupMembershipUpdate. chat-id membership-updates message))}})) (defn serialize "Serializes a record implementing the StatusMessage protocol using the custom writers" diff --git a/src/status_im/transport/message/v1/core.cljs b/src/status_im/transport/message/v1/core.cljs index c0015cefa7..d7e16bee50 100644 --- a/src/status_im/transport/message/v1/core.cljs +++ b/src/status_im/transport/message/v1/core.cljs @@ -1,15 +1,12 @@ (ns status-im.transport.message.v1.core (:require [status-im.transport.message.core :as message] + [taoensso.timbre :as log] [cljs.spec.alpha :as spec])) (defrecord GroupMembershipUpdate - [chat-id chat-name admin participants leaves version signature message] + [chat-id membership-updates message] message/StatusMessage (validate [this] - (when (spec/valid? :message/group-membership-update this) - this))) - -(defrecord GroupLeave - [] - message/StatusMessage - (validate [this] this)) + (if (spec/valid? :message/group-membership-update this) + this + (log/warn "failed group membership validation" (spec/explain :message/group-membership-update this))))) diff --git a/src/status_im/ui/components/contact/contact.cljs b/src/status_im/ui/components/contact/contact.cljs index 1d0ad036c3..90a68114ad 100644 --- a/src/status_im/ui/components/contact/contact.cljs +++ b/src/status_im/ui/components/contact/contact.cljs @@ -1,6 +1,7 @@ (ns status-im.ui.components.contact.contact (:require-macros [status-im.utils.views :as views]) (:require [status-im.i18n :as i18n] + [status-im.utils.platform :as platform] [status-im.ui.components.react :as react] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.chat-icon.screen :as chat-icon] @@ -9,6 +10,15 @@ [status-im.ui.components.list.views :as list] [status-im.utils.gfycat.core :as gfycat])) +(defn desktop-extended-options [options] + [react/view {} + (doall (for [{:keys [label action]} options] + ^{:key label} + [react/touchable-highlight + {:on-press action} + [react/view {} + [react/text {} label]]]))]) + (defn- contact-inner-view ([{:keys [info style props] {:keys [whisper-identity name dapp?] :as contact} :contact}] [react/view (merge styles/contact-inner-container style) @@ -39,12 +49,14 @@ [react/view styles/forward-btn [vector-icons/icon :icons/forward]]) (when (and extended? (not (empty? extend-options))) - [react/view styles/more-btn-container - [react/touchable-highlight {:on-press #(list-selection/show {:options extend-options - :title extend-title}) - :accessibility-label :menu-option} - [react/view styles/more-btn - [vector-icons/icon :icons/options {:accessibility-label :options}]]]])]]) + (if platform/desktop? + (desktop-extended-options extend-options) + [react/view styles/more-btn-container + [react/touchable-highlight {:on-press #(list-selection/show {:options extend-options + :title extend-title}) + :accessibility-label :menu-option} + [react/view styles/more-btn + [vector-icons/icon :icons/options {:accessibility-label :options}]]]]))]]) (views/defview toogle-contact-view [{:keys [whisper-identity] :as contact} selected-key on-toggle-handler] (views/letsubs [checked [selected-key whisper-identity]] diff --git a/src/status_im/ui/screens/chat/actions.cljs b/src/status_im/ui/screens/chat/actions.cljs index 007bcaa412..808620f00c 100644 --- a/src/status_im/ui/screens/chat/actions.cljs +++ b/src/status_im/ui/screens/chat/actions.cljs @@ -28,7 +28,10 @@ (defn- delete-chat [chat-id group?] {:label (i18n/label :t/delete-chat) - :action #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id group?])}) + :action #(re-frame/dispatch [(if group? + :group-chats.ui/remove-chat-pressed + :chat.ui/remove-chat-pressed) + chat-id])}) (defn- chat-actions [chat-id] [view-my-wallet diff --git a/src/status_im/ui/screens/contacts/subs.cljs b/src/status_im/ui/screens/contacts/subs.cljs index d38071d6bd..5bcceb8b50 100644 --- a/src/status_im/ui/screens/contacts/subs.cljs +++ b/src/status_im/ui/screens/contacts/subs.cljs @@ -93,8 +93,8 @@ (:name current-account) (:name (contacts identity)))))) -(defn query-chat-contacts [[{:keys [contacts group-admin]} all-contacts] [_ query-fn]] - (let [participant-set (into #{} (filter identity) (conj contacts group-admin))] +(defn query-chat-contacts [[{:keys [contacts]} all-contacts] [_ query-fn]] + (let [participant-set (into #{} (filter identity) contacts)] (query-fn (comp participant-set :whisper-identity) (vals all-contacts)))) (reg-sub :query-current-chat-contacts @@ -104,24 +104,26 @@ (reg-sub :get-all-contacts-not-in-current-chat :<- [:query-current-chat-contacts remove] - identity) + (fn [contacts] + (remove :dapp? contacts))) -(defn get-all-contacts-in-group-chat [chat-contact-ids group-admin-id contacts current-account] - (let [participant-set (into #{} (filter identity) (conj chat-contact-ids group-admin-id)) - current-account-contact (-> current-account +(defn get-all-contacts-in-group-chat [members contacts current-account] + (let [current-account-contact (-> current-account (select-keys [:name :photo-path :public-key]) (clojure.set/rename-keys {:public-key :whisper-identity})) all-contacts (assoc contacts (:whisper-identity current-account-contact) current-account-contact)] - (map #(or (get all-contacts %) - (utils.contacts/whisper-id->new-contact %)) - participant-set))) + (->> members + (map #(or (get all-contacts %) + (utils.contacts/whisper-id->new-contact %))) + (remove :dapp?) + (sort-by (comp clojure.string/lower-case :name))))) (reg-sub :get-current-chat-contacts :<- [:get-current-chat] :<- [:get-contacts] :<- [:get-current-account] - (fn [[{:keys [contacts group-admin]} all-contacts current-account]] - (get-all-contacts-in-group-chat contacts group-admin all-contacts current-account))) + (fn [[{:keys [contacts]} all-contacts current-account]] + (get-all-contacts-in-group-chat contacts all-contacts current-account))) (reg-sub :get-contacts-by-chat (fn [[_ _ chat-id] _] diff --git a/src/status_im/ui/screens/desktop/main/chat/views.cljs b/src/status_im/ui/screens/desktop/main/chat/views.cljs index 377524a6fb..d9eb9cef70 100644 --- a/src/status_im/ui/screens/desktop/main/chat/views.cljs +++ b/src/status_im/ui/screens/desktop/main/chat/views.cljs @@ -59,6 +59,10 @@ [react/text {:style (styles/profile-actions-text colors/black) :on-press #(re-frame/dispatch [:show-profile-desktop whisper-identity])} (i18n/label :t/view-profile)]) + (when (and group-chat (not public?)) + [react/text {:style (styles/profile-actions-text colors/black) + :on-press #(re-frame/dispatch [:show-group-chat-profile])} + (i18n/label :t/group-info)]) [react/text {:style (styles/profile-actions-text colors/black) :on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed])} (i18n/label :t/clear-history)] diff --git a/src/status_im/ui/screens/desktop/views.cljs b/src/status_im/ui/screens/desktop/views.cljs index 14209cd21e..1c7b1b0508 100644 --- a/src/status_im/ui/screens/desktop/views.cljs +++ b/src/status_im/ui/screens/desktop/views.cljs @@ -5,7 +5,8 @@ [status-im.ui.screens.intro.views :as intro.views] [status-im.ui.screens.group.add-contacts.views :refer [contact-toggle-list]] [status-im.ui.screens.group.views :refer [new-group]] - + [status-im.ui.screens.profile.group-chat.views :refer [group-chat-profile]] + [status-im.ui.screens.group.add-contacts.views :refer [add-participants-toggle-list]] [status-im.ui.screens.accounts.create.views :as create.views] [status-im.ui.screens.accounts.login.views :as login.views] [status-im.ui.screens.accounts.recover.views :as recover.views] @@ -22,6 +23,9 @@ :create-account create.views/create-account :new-group new-group :contact-toggle-list contact-toggle-list + :group-chat-profile group-chat-profile + :add-participants-toggle-list add-participants-toggle-list + (:new-contact :advanced-settings :chat diff --git a/src/status_im/ui/screens/group/add_contacts/views.cljs b/src/status_im/ui/screens/group/add_contacts/views.cljs index 549562eb17..5ea1c8757c 100644 --- a/src/status_im/ui/screens/group/add_contacts/views.cljs +++ b/src/status_im/ui/screens/group/add_contacts/views.cljs @@ -44,13 +44,14 @@ (defview contact-toggle-list [] (letsubs [contacts [:all-added-people-contacts] selected-contacts-count [:selected-contacts-count]] - [react/keyboard-avoiding-view {:style styles/group-container} - [status-bar] - [toggle-list-toolbar {:handler #(re-frame/dispatch [:navigate-to :new-group]) - :label (i18n/label :t/next) - :count (pos? selected-contacts-count)} - (i18n/label :t/group-chat)] - [toggle-list contacts group-toggle-contact]])) + (when (seq contacts) + [react/keyboard-avoiding-view {:style styles/group-container} + [status-bar] + [toggle-list-toolbar {:handler #(re-frame/dispatch [:navigate-to :new-group]) + :label (i18n/label :t/next) + :count (pos? selected-contacts-count)} + (i18n/label :t/group-chat)] + [toggle-list contacts group-toggle-contact]]))) ;; Add participants to existing group chat (defview add-participants-toggle-list [] @@ -61,8 +62,9 @@ [status-bar] [toggle-list-toolbar {:count selected-contacts-count :handler #(do - (re-frame/dispatch [:add-new-group-chat-participants]) + (re-frame/dispatch [:group-chats.ui/add-members-pressed]) (re-frame/dispatch [:navigate-back])) :label (i18n/label :t/add)} name] - [toggle-list contacts group-toggle-participant]])) + (when (seq contacts) + [toggle-list contacts group-toggle-participant])])) diff --git a/src/status_im/ui/screens/group/core.cljs b/src/status_im/ui/screens/group/core.cljs deleted file mode 100644 index 71d92b3784..0000000000 --- a/src/status_im/ui/screens/group/core.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.ui.screens.group.core - (:require [status-im.data-store.chats :as chats-store] - [status-im.utils.fx :as fx])) - -(fx/defn participants-added - [{:keys [db] :as cofx} chat-id added-participants-set] - (when (seq added-participants-set) - {:db (update-in db [:chats chat-id :contacts] - concat added-participants-set) - :data-store/tx [(chats-store/add-chat-contacts-tx - chat-id added-participants-set)]})) - -(fx/defn participants-removed - [{:keys [now db] :as cofx} chat-id removed-participants-set] - (when (seq removed-participants-set) - (let [{:keys [is-active timestamp]} (get-in db [:chats chat-id])] - ;;TODO: not sure what this condition is for - (when (and is-active (>= now timestamp)) - {:db (update-in db [:chats chat-id :contacts] - (partial remove removed-participants-set)) - :data-store/tx [(chats-store/remove-chat-contacts-tx - chat-id removed-participants-set)]})))) diff --git a/src/status_im/ui/screens/profile/components/styles.cljs b/src/status_im/ui/screens/profile/components/styles.cljs index 5635b478a3..0975e8619b 100644 --- a/src/status_im/ui/screens/profile/components/styles.cljs +++ b/src/status_im/ui/screens/profile/components/styles.cljs @@ -31,6 +31,8 @@ {:font-size 15 :text-align :center :flex 1 + :desktop {:height 20 + :width 200} :ios {:letter-spacing -0.2 :margin-top 1 :height 45 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 403d4732c9..9546d920ef 100644 --- a/src/status_im/ui/screens/profile/group_chat/views.cljs +++ b/src/status_im/ui/screens/profile/group_chat/views.cljs @@ -1,6 +1,7 @@ (ns status-im.ui.screens.profile.group-chat.views (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [status-im.ui.screens.profile.group-chat.styles :as styles] + (:require [status-im.utils.platform :as platform] + [status-im.ui.screens.profile.group-chat.styles :as styles] [status-im.ui.components.react :as react] [status-im.ui.screens.profile.components.styles :as profile.components.styles] [status-im.ui.screens.profile.components.views :as profile.components] @@ -37,57 +38,59 @@ (defn actions [admin? chat-id] (concat - #_(when admin? - [{:label (i18n/label :add-members) - :icon :icons/add - :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) + (when admin? + [{:label (i18n/label :add-members) + :icon :icons/add + :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) [{:label (i18n/label :t/clear-history) :icon :icons/close :action #(re-frame/dispatch [:chat.ui/clear-history-pressed]) :accessibility-label :clear-history-button} {:label (i18n/label :t/delete-chat) :icon :icons/arrow-left - :action #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id]) + :action #(re-frame/dispatch [:group-chats.ui/remove-chat-pressed chat-id]) :accessibility-label :delete-chat-button}])) -(defn contact-actions [contact] - [{:action #(re-frame/dispatch [:chat.ui/show-profile (:whisper-identity contact)]) +(defn member-actions [chat-id member] + [{:action #(re-frame/dispatch [(if platform/desktop? :show-profile-desktop :chat.ui/show-profile) (:whisper-identity member)]) :label (i18n/label :t/view-profile)} - #_{:action #(re-frame/dispatch [:remove-group-chat-participants #{(:whisper-identity contact)}]) - :label (i18n/label :t/remove-from-chat)}]) + {:action #(re-frame/dispatch [:group-chats.ui/remove-member-pressed chat-id (:whisper-identity member)]) + :label (i18n/label :t/remove-from-chat)}]) -(defn render-contact [{:keys [name whisper-identity] :as contact} admin? group-admin-identity current-user-identity] +(defn render-member [chat-id {:keys [name whisper-identity] :as member} admin? current-user-identity] [react/view [contact/contact-view - {:contact contact - :extend-options (contact-actions contact) + {:contact member + :extend-options (member-actions chat-id member) :extend-title name - :extended? (and admin? (not= whisper-identity group-admin-identity)) + :extended? (and admin? + (not= whisper-identity current-user-identity)) :accessibility-label :member-item :inner-props {:accessibility-label :member-name-text} :on-press (when (not= whisper-identity current-user-identity) - #(re-frame/dispatch [:chat.ui/show-profile whisper-identity]))}]]) + #(re-frame/dispatch [(if platform/desktop? :show-profile-desktop :chat.ui/show-profile) whisper-identity]))}]]) -(defview chat-group-contacts-view [admin? group-admin-identity current-user-identity] - (letsubs [contacts [:get-current-chat-contacts]] - [react/view - [list/flat-list {:data contacts - :separator list/default-separator - :key-fn :address - :render-fn #(render-contact % admin? group-admin-identity current-user-identity)}]])) +(defview chat-group-members-view [chat-id admin? current-user-identity] + (letsubs [members [:get-current-chat-contacts]] + (when (seq members) + [react/view + [list/flat-list {:data members + :separator list/default-separator + :key-fn :address + :render-fn #(render-member chat-id % admin? current-user-identity)}]]))) -(defn members-list [admin? group-admin-identity current-user-identity] +(defn members-list [chat-id admin? current-user-identity] [react/view [profile.components/settings-title (i18n/label :t/members-title)] - [chat-group-contacts-view admin? group-admin-identity current-user-identity]]) + [chat-group-members-view chat-id admin? current-user-identity]]) (defview group-chat-profile [] - (letsubs [{:keys [group-admin] :as current-chat} [:get-current-chat] + (letsubs [{:keys [admins chat-id] :as current-chat} [:get-current-chat] editing? [:get :group-chat-profile/editing?] changed-chat [:get :group-chat-profile/profile] current-pk [:get :current-public-key]] (let [shown-chat (merge current-chat changed-chat) - admin? (= current-pk group-admin)] + admin? (admins current-pk)] [react/view profile.components.styles/profile [status-bar/status-bar] (if editing? @@ -100,10 +103,10 @@ :editing? editing? :allow-icon-change? false :on-change-text-event :group-chats.ui/name-changed}] - [list/action-list (actions admin? (:chat-id current-chat)) + [list/action-list (actions admin? chat-id) {:container-style styles/action-container :action-style styles/action :action-label-style styles/action-label :action-separator-style styles/action-separator :icon-opts styles/action-icon-opts}] - [members-list admin? group-admin current-pk]]]]))) + [members-list chat-id admin? (first admins) current-pk]]]]))) diff --git a/src/status_im/utils/fx.cljs b/src/status_im/utils/fx.cljs index ca4bc9b9c1..d2550cd647 100644 --- a/src/status_im/utils/fx.cljs +++ b/src/status_im/utils/fx.cljs @@ -13,7 +13,7 @@ #{:data-store/tx :data-store/base-tx :chat-received-message/add-fx :shh/add-new-sym-keys :shh/get-new-sym-keys :shh/post :shh/generate-sym-key-from-password :confirm-messages-processed - :group-chats/verify-membership-signature :utils/dispatch-later}) + :group-chats/extract-membership-signature :utils/dispatch-later}) (defn- safe-merge [fx new-fx] (if (:merging-fx-with-common-keys fx) diff --git a/test/cljs/status_im/test/chat/models.cljs b/test/cljs/status_im/test/chat/models.cljs index dab1da95ff..8b29c93188 100644 --- a/test/cljs/status_im/test/chat/models.cljs +++ b/test/cljs/status_im/test/chat/models.cljs @@ -46,20 +46,7 @@ (testing "it updates existins props" (is (= "new-name" (:name actual-chat)))) (testing "it adds the fx to store a chat" - (is store-chat-fx)))) - (testing "upserting a deleted chat" - (let [chat-id "some-chat-id" - contact-name "contact-name" - chat-props {:chat-id chat-id - :name "new-name" - :extra-prop "some"} - cofx {:some-cofx "b" - :db {:chats {chat-id {:is-active false - :name "old-name"}}}}] - (testing "it updates it if is-active is passed" - (is (get-in (chat/upsert-chat cofx (assoc chat-props :is-active true)) [:db :chats chat-id :is-active]))) - (testing "it returns the db unchanged" - (is (= {:db (:db cofx)} (chat/upsert-chat cofx chat-props))))))) + (is store-chat-fx))))) (deftest add-group-chat (let [chat-id "chat-id" @@ -94,7 +81,7 @@ (testing "it sets the name" (is (= topic (:name chat)))) (testing "it sets the participants" - (is (= [] (:contacts chat)))) + (is (= #{} (:contacts chat)))) (testing "it sets the chat-id" (is (= topic (:chat-id chat)))) (testing "it sets the group-chat flag" diff --git a/test/cljs/status_im/test/contacts/subs.cljs b/test/cljs/status_im/test/contacts/subs.cljs index 1de2a0aa91..75fe2e52df 100644 --- a/test/cljs/status_im/test/contacts/subs.cljs +++ b/test/cljs/status_im/test/contacts/subs.cljs @@ -7,8 +7,8 @@ (testing "get-all-contacts-in-group-chat" (with-redefs [identicon/identicon (constantly "generated")] (let [chat-contact-ids ["0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917" + "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f" "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"] - group-admin-id "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f" contacts {"demo-bot" {:description nil, :last-updated 0, @@ -76,7 +76,6 @@ :public-key "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"}] (is (= (contacts-subs/get-all-contacts-in-group-chat chat-contact-ids - group-admin-id contacts current-account) [{:name "Snappy Impressive Leonberger" diff --git a/test/cljs/status_im/test/data_store/chats.cljs b/test/cljs/status_im/test/data_store/chats.cljs new file mode 100644 index 0000000000..19ca609f82 --- /dev/null +++ b/test/cljs/status_im/test/data_store/chats.cljs @@ -0,0 +1,50 @@ +(ns status-im.test.data-store.chats + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.random :as utils.random] + [status-im.data-store.chats :as chats])) + +(deftest normalize-chat-test + (testing "admins & contacts" + (with-redefs [chats/get-last-clock-value (constantly 42)] + (is (= {:last-clock-value 42 + :admins #{4} + :contacts #{2} + :membership-updates []} + (chats/normalize-chat {:admins [4] + :contacts [2]}))))) + (testing "membership-updates" + (with-redefs [chats/get-last-clock-value (constantly 42)] + (let [raw-events {"1" {:id "1" :type "members-added" :clock-value 10 :members [1 2] :signature "a" :from "id-1"} + "2" {:id "2" :type "member-removed" :clock-value 11 :member 1 :signature "a" :from "id-1"} + "3" {:id "3" :type "chat-created" :clock-value 0 :name "blah" :signature "b" :from "id-2"}} + expected #{{:chat-id "chat-id" + :from "id-2" + :signature "b" + :events [{:type "chat-created" :clock-value 0 :name "blah"}]} + {:chat-id "chat-id" + :signature "a" + :from "id-1" + :events [{:type "members-added" :clock-value 10 :members [1 2]} + {:type "member-removed" :clock-value 11 :member 1}]}} + actual (->> (chats/normalize-chat {:chat-id "chat-id" + :membership-updates raw-events}) + :membership-updates + (into #{}))] + (is (= expected + actual)))))) + +(deftest marshal-membership-updates-test + (let [raw-updates [{:chat-id "chat-id" + :signature "b" + :from "id-1" + :events [{:type "chat-created" :clock-value 0 :name "blah"}]} + {:chat-id "chat-id" + :signature "a" + :from "id-2" + :events [{:type "members-added" :clock-value 10 :members [1 2]} + {:type "member-removed" :clock-value 11 :member 1}]}] + expected #{{:type "members-added" :clock-value 10 :from "id-2" :members [1 2] :signature "a" :id "0xb7690375de21da4890d2d5acca8b56e327d9eb75fd3b4bcceca4bf1679c2f830"} + {:type "member-removed" :clock-value 11 :from "id-2" :member 1 :signature "a" :id "0x2a66f195abf6e6903c4245e372e1e2e6aea2b2c0a74ad03080a313e94197a64f"} + {:type "chat-created" :clock-value 0 :from "id-1" :name "blah" :signature "b" :id "0x7fad22accf1dec64daedf83e7af19b0dcde8c5facfb479874a48da5fb6967e07"}} + actual (into #{} (chats/marshal-membership-updates raw-updates))] + (is (= expected actual)))) diff --git a/test/cljs/status_im/test/group_chats/core.cljs b/test/cljs/status_im/test/group_chats/core.cljs index d0983b9aa1..9a940bf058 100644 --- a/test/cljs/status_im/test/group_chats/core.cljs +++ b/test/cljs/status_im/test/group_chats/core.cljs @@ -1,5 +1,6 @@ (ns status-im.test.group-chats.core (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.clocks :as utils.clocks] [status-im.utils.config :as config] [status-im.group-chats.core :as group-chats])) @@ -8,24 +9,24 @@ (def member-1 "member-1") (def member-2 "member-2") +(def member-3 "member-3") +(def member-4 "member-4") (def admin member-1) (def chat-id (str random-id admin)) -(def invitation-m1 {:id "m-1" - :user member-1}) -(def invitation-m2 {:id "m-2" - :user member-2}) - (def initial-message {:chat-id chat-id - :chat-name chat-name - :admin admin - :participants [invitation-m1 - invitation-m2] - :leaves [] - :signature "some" - :version 1}) + :membership-updates [{:from admin + :events [{:type "chat-created" + :name "chat-name" + :clock-value 1} + {:type "members-added" + :clock-value 3 + :members [member-2 member-3]}]}]}) + +(deftest get-last-clock-value-test + (is (= 3 (group-chats/get-last-clock-value {:db {:chats {chat-id initial-message}}} chat-id)))) (deftest handle-group-membership-update (with-redefs [config/group-chats-enabled? true] @@ -37,13 +38,21 @@ (get chat-id))] (testing "it creates a new chat" (is actual)) + (testing "it sets the right chat-name" + (is (= "chat-name" + (:name actual)))) + (testing "it sets the right chat-id" + (is (= chat-id + (:chat-id actual)))) (testing "it sets the right participants" - (is (= [invitation-m1 - invitation-m2] + (is (= #{member-1 member-2 member-3} (:contacts actual)))) - (testing "it sets the right version" - (is (= 1 - (:membership-version actual)))))) + (testing "it sets the updates" + (is (= (:membership-updates initial-message) + (:membership-updates actual)))) + (testing "it sets the right admins" + (is (= #{admin} + (:admins actual)))))) (testing "a chat with the wrong id" (let [bad-chat-id (str random-id member-2) actual (-> @@ -57,16 +66,325 @@ (testing "it does not create a chat" (is (not actual))))) (testing "an already existing chat" - (let [cofx {:db {:chats {chat-id {:contacts [invitation-m1 - invitation-m2] - :group-admin admin - :membership-version 2}}}}] - (testing "an update from the admin is received" - (testing "the message is an older version" - (let [actual (group-chats/handle-membership-update cofx initial-message admin)] - (testing "it noops" - (is (= actual cofx))))) - (testing "the message is a more recent version" - (testing "it sets the right participants"))) - (testing "a leave from a member is received" - (testing "the user is removed")))))) + (let [cofx {:db {:chats {chat-id {:admins #{admin} + :name "chat-name" + :chat-id chat-id + :is-active true + :group-chat true + :contacts #{member-1 member-2 member-3} + :membership-updates (:membership-updates initial-message)}}}}] + (testing "the message has already been received" + (let [actual (group-chats/handle-membership-update cofx initial-message admin)] + (testing "it noops" + (is (= (get-in actual [:db :chats chat-id]) + (get-in cofx [:db :chats chat-id])))))) + (testing "a new message comes in" + (let [actual (group-chats/handle-membership-update cofx + {:chat-id chat-id + :membership-updates [{:from member-1 + :events [{:type "chat-created" + :clock-value 1 + :name "group-name"} + {:type "admins-added" + :clock-value 10 + :members [member-2]} + {:type "admin-removed" + :clock-value 11 + :member member-1}]} + {:from member-2 + :events [{:type "member-removed" + :clock-value 12 + :member member-3} + {:type "members-added" + :clock-value 12 + :members [member-4]}]}]} + member-3) + actual-chat (get-in actual [:db :chats chat-id])] + (testing "the chat is updated" + (is actual-chat)) + (testing "admins are updated" + (is (= #{member-2} (:admins actual-chat)))) + (testing "members are updated" + (is (= #{member-1 member-2 member-4} (:contacts actual-chat)))))))))) + +(deftest build-group-test + (testing "only adds" + (let [events [{:type "chat-created" + :clock-value 0 + :name "chat-name" + :from "1"} + {:type "members-added" + :clock-value 1 + :from "1" + :members ["2"]} + {:type "admins-added" + :clock-value 2 + :from "1" + :members ["2"]} + {:type "members-added" + :clock-value 3 + :from "2" + :members ["3"]}] + expected {:name "chat-name" + :admins #{"1" "2"} + :contacts #{"1" "2" "3"}}] + (is (= expected (group-chats/build-group events))))) + (testing "adds and removes" + (let [events [{:type "chat-created" + :clock-value 0 + :name "chat-name" + :from "1"} + {:type "members-added" + :clock-value 1 + :from "1" + :members ["2"]} + {:type "admins-added" + :clock-value 2 + :from "1" + :members ["2"]} + {:type "admin-removed" + :clock-value 3 + :from "2" + :member "2"} + {:type "member-removed" + :clock-value 4 + :from "2" + :member "2"}] + expected {:name "chat-name" + :admins #{"1"} + :contacts #{"1"}}] + (is (= expected (group-chats/build-group events))))) + (testing "name changed" + (let [events [{:type "chat-created" + :clock-value 0 + :name "chat-name" + :from "1"} + {:type "members-added" + :clock-value 1 + :from "1" + :members ["2"]} + {:type "admins-added" + :clock-value 2 + :from "1" + :members ["2"]} + {:type "name-changed" + :clock-value 3 + :from "2" + :name "new-name"}] + expected {:name "new-name" + :admins #{"1" "2"} + :contacts #{"1" "2"}}] + (is (= expected (group-chats/build-group events))))) + (testing "invalid events" + (let [events [{:type "chat-created" + :name "chat-name" + :clock-value 0 + :from "1"} + {:type "admins-added" ; can't make an admin a user not in the group + :clock-value 1 + :from "1" + :members ["non-existing"]} + {:type "members-added" + :clock-value 2 + :from "1" + :members ["2"]} + {:type "admins-added" + :clock-value 3 + :from "1" + :members ["2"]} + {:type "members-added" + :clock-value 4 + :from "2" + :members ["3"]} + {:type "admin-removed" ; can't remove an admin from admins unless it's the same user + :clock-value 5 + :from "1" + :member "2"} + {:type "member-removed" ; can't remove an admin from the group + :clock-value 6 + :from "1" + :member "2"}] + expected {:name "chat-name" + :admins #{"1" "2"} + :contacts #{"1" "2" "3"}}] + (is (= expected (group-chats/build-group events))))) + (testing "out of order-events" + (let [events [{:type "chat-created" + :name "chat-name" + :clock-value 0 + :from "1"} + {:type "admins-added" + :clock-value 2 + :from "1" + :members ["2"]} + {:type "members-added" + :clock-value 1 + :from "1" + :members ["2"]} + {:type "members-added" + :clock-value 3 + :from "2" + :members ["3"]}] + expected {:name "chat-name" + :admins #{"1" "2"} + :contacts #{"1" "2" "3"}}] + (is (= expected (group-chats/build-group events)))))) + +(deftest valid-event-test + (let [multi-admin-group {:admins #{"1" "2"} + :contacts #{"1" "2" "3"}} + single-admin-group {:admins #{"1"} + :contacts #{"1" "2" "3"}}] + (testing "members-added" + (testing "admins can add members" + (is (group-chats/valid-event? multi-admin-group + {:type "members-added" :clock-value 6 :from "1" :members ["4"]}))) + (testing "non-admin members cannot add members" + (is (not (group-chats/valid-event? multi-admin-group + {:type "members-added" :clock-value 6 :from "3" :members ["4"]}))))) + (testing "admins-added" + (testing "admins can make other member admins" + (is (group-chats/valid-event? multi-admin-group + {:type "admins-added" :clock-value 6 :from "1" :members ["3"]}))) + (testing "non-admins can't make other member admins" + (is (not (group-chats/valid-event? multi-admin-group + {:type "admins-added" :clock-value 6 :from "3" :members ["3"]})))) + (testing "non-existing users can't be made admin" + (is (not (group-chats/valid-event? multi-admin-group + {:type "admins-added" :clock-value 6 :from "1" :members ["not-existing"]}))))) + (testing "member-removed" + (testing "admins can remove non-admin members" + (is (group-chats/valid-event? multi-admin-group + {:type "member-removed" :clock-value 6 :from "1" :member "3"}))) + (testing "admins can't remove themselves" + (is (not (group-chats/valid-event? multi-admin-group + {:type "member-removed" :clock-value 6 :from "1" :member "1"})))) + (testing "participants non-admin can remove themselves" + (is (group-chats/valid-event? multi-admin-group + {:type "member-removed" :clock-value 6 :from "3" :member "3"}))) + (testing "non-admin can't remove other members" + (is (not (group-chats/valid-event? multi-admin-group + {:type "member-removed" :clock-value 6 :from "3" :member "1"}))))) + (testing "admin-removed" + (testing "admins can remove themselves" + (is (group-chats/valid-event? multi-admin-group + {:type "admin-removed" :clock-value 6 :from "1" :member "1"}))) + (testing "admins can't remove other admins" + (is (not (group-chats/valid-event? multi-admin-group + {:type "admin-removed" :clock-value 6 :from "1" :member "2"})))) + (testing "participants non-admin can't remove other admins" + (is (not (group-chats/valid-event? multi-admin-group + {:type "admin-removed" :clock-value 6 :from "3" :member "1"})))) + (testing "the last admin can't be removed" + (is (not (group-chats/valid-event? single-admin-group + {:type "admin-removed" :clock-value 6 :from "1" :member "1"})))) + (testing "name-changed" + (testing "a change from an admin" + (is (group-chats/valid-event? multi-admin-group + {:type "name-changed" :clock-value 6 :from "1" :name "new-name"})))) + (testing "a change from an non-admin" + (is (not (group-chats/valid-event? multi-admin-group + {:type "name-changed" :clock-value 6 :from "3" :name "new-name"})))) + (testing "an empty name" + (is (not (group-chats/valid-event? multi-admin-group + {:type "name-changed" :clock-value 6 :from "1" :name " "}))))))) + +(deftest create-test + (testing "create a new chat" + (with-redefs [utils.clocks/send inc] + (let [cofx {:random-guid-generator (constantly "random") + :db {:current-public-key "me" + :group/selected-contacts #{"1" "2"}}}] + (is (= {:chat-id "randomme" + :from "me" + :events [{:type "chat-created" + :clock-value 1 + :name "group-name"} + {:type "members-added" + :clock-value 2 + :members #{"1" "2"}}]} + (:group-chats/sign-membership (group-chats/create cofx "group-name")))))))) + +(deftest signature-pairs-test + (let [event-1 {:from "1" + :signature "signature-1" + :events [{:type "a" :name "a" :clock-value 1} + {:type "b" :name "b" :clock-value 2}]} + event-2 {:from "2" + :signature "signature-2" + :events [{:type "c" :name "c" :clock-value 1} + {:type "d" :name "d" :clock-value 2}]} + message {:chat-id "randomme" + + :membership-updates [event-1 + event-2]} + expected (js/JSON.stringify + (clj->js [[(group-chats/signature-material "randomme" (:events event-1)) + "signature-1"] + [(group-chats/signature-material "randomme" (:events event-2)) + "signature-2"]]))] + + (is (= expected (group-chats/signature-pairs message))))) + +(deftest signature-material-test + (is (= (js/JSON.stringify (clj->js [[[["a" "a-value"] + ["b" "b-value"] + ["c" "c-value"]] + [["a" "a-value"] + ["e" "e-value"]]] "chat-id"])) + (group-chats/signature-material "chat-id" [{:b "b-value" + :a "a-value" + :c "c-value"} + {:e "e-value" + :a "a-value"}])))) + +(deftest remove-group-chat-test + (with-redefs [utils.clocks/send inc] + (let [cofx {:db {:chats {chat-id {:admins #{admin} + :name "chat-name" + :chat-id chat-id + :is-active true + :group-chat true + :contacts #{member-1 member-2 member-3} + :membership-updates (:membership-updates initial-message)}}}}] + (testing "removing a member" + (is (= {:from member-3 + :chat-id chat-id + :events [{:type "member-removed" :member member-3 :clock-value 4}]} + (:group-chats/sign-membership + (group-chats/remove + (assoc-in cofx [:db :current-public-key] member-3) + chat-id))))) + (testing "removing an admin" + (is (not (:group-chats/sign-membership + (group-chats/remove + (assoc-in cofx [:db :current-public-key] member-1) + chat-id)))))))) + +(deftest add-members-test + (with-redefs [utils.clocks/send inc] + (testing "add-members" + (let [cofx {:db {:current-chat-id chat-id + :selected-participants ["new-member"] + :current-public-key "me" + :chats {chat-id {:membership-updates [{:events [{:clock-value 1}]}]}}}}] + (is (= {:chat-id chat-id + :from "me" + :events [{:type "members-added" + :clock-value 2 + :members ["new-member"]}]} + (:group-chats/sign-membership (group-chats/add-members cofx)))))))) + +(deftest remove-member-test + (with-redefs [utils.clocks/send inc] + (testing "remove-member" + (let [cofx {:db {:current-public-key "me" + :chats {chat-id {:admins #{"me"} + :contacts #{"member"} + :membership-updates [{:events [{:clock-value 1}]}]}}}}] + (is (= {:chat-id chat-id + :from "me" + :events [{:type "member-removed" + :clock-value 2 + :member "member"}]} + (:group-chats/sign-membership (group-chats/remove-member cofx chat-id "member")))))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 3626500bbe..2f452dad7d 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -2,6 +2,7 @@ (:require [doo.runner :refer-macros [doo-tests]] [status-im.test.contacts.events] [status-im.test.contacts.subs] + [status-im.test.data-store.chats] [status-im.test.data-store.realm.core] [status-im.test.browser.core] [status-im.test.browser.permissions] @@ -72,6 +73,7 @@ 'status-im.test.contacts.events 'status-im.test.contacts.subs 'status-im.test.init.core + 'status-im.test.data-store.chats 'status-im.test.data-store.realm.core 'status-im.test.mailserver.core 'status-im.test.group-chats.core