Allow to edit group membership
Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
parent
66ef32d3b9
commit
78b6d67d79
|
@ -1 +1 @@
|
|||
0.16.0
|
||||
0.16.1
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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})))
|
||||
: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)
|
||||
|
|
|
@ -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 ", ")))
|
|
@ -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)]
|
||||
|
|
|
@ -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- normalize-chat [{:keys [chat-id] :as chat}]
|
||||
(let [last-clock-value (-> (core/get-by-field @core/account-realm
|
||||
(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)]
|
||||
:clock-value))
|
||||
|
||||
(defn- normalize-chat [{:keys [chat-id] :as chat}]
|
||||
(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
|
||||
|
|
|
@ -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}}})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}})
|
|
@ -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"))
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
:membership-updates (:membership-updates 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})))
|
||||
|
||||
(defn send-membership-update
|
||||
"Send a membership update to all participants but the sender"
|
||||
[cofx payload chat-id]
|
||||
(let [{:keys [participants]} payload
|
||||
([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 participants 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}})))
|
||||
|
||||
(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]}))
|
||||
: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))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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)))
|
||||
(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}]]]])]])
|
||||
[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]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
(->> members
|
||||
(map #(or (get all-contacts %)
|
||||
(utils.contacts/whisper-id->new-contact %))
|
||||
participant-set)))
|
||||
(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] _]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -44,13 +44,14 @@
|
|||
(defview contact-toggle-list []
|
||||
(letsubs [contacts [:all-added-people-contacts]
|
||||
selected-contacts-count [:selected-contacts-count]]
|
||||
(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]]))
|
||||
[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])]))
|
||||
|
|
|
@ -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)]}))))
|
|
@ -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
|
||||
|
|
|
@ -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,7 +38,7 @@
|
|||
|
||||
(defn actions [admin? chat-id]
|
||||
(concat
|
||||
#_(when admin?
|
||||
(when admin?
|
||||
[{:label (i18n/label :add-members)
|
||||
:icon :icons/add
|
||||
:action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}])
|
||||
|
@ -47,47 +48,49 @@
|
|||
: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)}])
|
||||
{: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]]
|
||||
(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 contacts
|
||||
[list/flat-list {:data members
|
||||
:separator list/default-separator
|
||||
:key-fn :address
|
||||
:render-fn #(render-contact % admin? group-admin-identity current-user-identity)}]]))
|
||||
: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]]]])))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))))
|
|
@ -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 [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 (= 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"))))))
|
||||
(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"))))))))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue