mirror of
https://github.com/status-im/status-mobile.git
synced 2025-02-14 17:47:18 +00:00
Move chats to status-go
This commit moves chats to status-go. I have changed the logic to load all chats in one go for simplicity and while that might have a performance impact, I think it's premature to optimize this flow as there will be more changes to the login flow. Also currently this is likely to be slower as we need to wait for the status-service to be initialized, as well as realm. No migration is provided as we are past the point of no return, so by installing this version you will lose your chats. Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
parent
f464269263
commit
6fa482a776
@ -101,8 +101,9 @@
|
||||
(or (get (:chats db) chat-id)
|
||||
(create-new-chat chat-id cofx))
|
||||
chat-props)]
|
||||
{:db (update-in db [:chats chat-id] merge chat)
|
||||
:data-store/tx [(chats-store/save-chat-tx chat)]}))
|
||||
(fx/merge cofx
|
||||
{:db (update-in db [:chats chat-id] merge chat)}
|
||||
(chats-store/save-chat-rpc chat))))
|
||||
|
||||
(fx/defn add-public-chat
|
||||
"Adds new public group chat to db & realm"
|
||||
@ -138,22 +139,25 @@
|
||||
:clock-value)
|
||||
deleted-at-clock-value
|
||||
(utils.clocks/send 0))]
|
||||
{:db (update-in db [:chats chat-id] merge
|
||||
{:messages empty-message-map
|
||||
:message-groups {}
|
||||
:last-message-content nil
|
||||
:last-message-content-type nil
|
||||
:unviewed-messages-count 0
|
||||
:deleted-at-clock-value last-message-clock-value})
|
||||
:data-store/tx [(chats-store/clear-history-tx chat-id last-message-clock-value)
|
||||
(messages-store/delete-chat-messages-tx chat-id)]}))
|
||||
(fx/merge
|
||||
cofx
|
||||
{:db (update-in db [:chats chat-id] merge
|
||||
{:messages empty-message-map
|
||||
:message-groups {}
|
||||
:last-message-content nil
|
||||
:last-message-content-type nil
|
||||
:unviewed-messages-count 0
|
||||
:deleted-at-clock-value last-message-clock-value})
|
||||
:data-store/tx [(messages-store/delete-chat-messages-tx chat-id)]}
|
||||
#(chats-store/save-chat-rpc % (get-in % [:db :chats chat-id])))))
|
||||
|
||||
(fx/defn deactivate-chat
|
||||
[{:keys [db now] :as cofx} chat-id]
|
||||
{:db (-> db
|
||||
(assoc-in [:chats chat-id :is-active] false)
|
||||
(assoc-in [:current-chat-id] nil))
|
||||
:data-store/tx [(chats-store/deactivate-chat-tx chat-id now)]})
|
||||
(fx/merge cofx
|
||||
{:db (-> db
|
||||
(assoc-in [:chats chat-id :is-active] false)
|
||||
(assoc-in [:current-chat-id] nil))}
|
||||
#(chats-store/save-chat-rpc % (get-in % [:db :chats chat-id]))))
|
||||
|
||||
(fx/defn remove-chat
|
||||
"Removes chat completely from app, producing all necessary effects for that"
|
||||
|
@ -1,6 +1,8 @@
|
||||
(ns status-im.chat.models.loading
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.data-store.chats :as data-store.chats]
|
||||
[status-im.chat.commands.core :as commands]
|
||||
[status-im.transport.filters.core :as filters]
|
||||
[status-im.chat.models :as chat-model]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.mailserver.core :as mailserver]
|
||||
@ -55,35 +57,7 @@
|
||||
|
||||
(fx/defn update-chats-in-app-db
|
||||
{:events [:chats-list/load-success]}
|
||||
[{:keys [db] :as cofx} chats]
|
||||
(fx/merge cofx
|
||||
{:db (assoc db :chats chats)}
|
||||
(commands/load-commands commands/register)))
|
||||
|
||||
(defn- unkeywordize-chat-names
|
||||
[chats]
|
||||
(reduce-kv
|
||||
(fn [acc chat-keyword chat]
|
||||
(assoc acc (name chat-keyword) chat))
|
||||
{}
|
||||
chats))
|
||||
|
||||
(defn load-chats-from-rpc
|
||||
[cofx]
|
||||
(fx/merge cofx
|
||||
{::json-rpc/call [{:method "status_chats"
|
||||
:params []
|
||||
:on-error
|
||||
#(log/error "can't retrieve chats list from status-go:" %)
|
||||
:on-success
|
||||
#(re-frame/dispatch
|
||||
[:chats-list/load-success
|
||||
(unkeywordize-chat-names (:chats %))])}]}))
|
||||
|
||||
(defn initialize-chats-legacy
|
||||
"Use realm + clojure to manage chats"
|
||||
[{:keys [db get-all-stored-chats] :as cofx}
|
||||
from to]
|
||||
[{:keys [db] :as cofx} new-chats]
|
||||
(let [old-chats (:chats db)
|
||||
chats (reduce (fn [acc {:keys [chat-id] :as chat}]
|
||||
(assoc acc chat-id
|
||||
@ -92,17 +66,26 @@
|
||||
:referenced-messages {}
|
||||
:messages empty-message-map)))
|
||||
{}
|
||||
(get-all-stored-chats from to))
|
||||
new-chats)
|
||||
chats (merge old-chats chats)]
|
||||
(update-chats-in-app-db cofx chats)))
|
||||
(fx/merge cofx
|
||||
{:db (assoc db :chats chats
|
||||
:chats/loading? false)}
|
||||
(filters/load-filters)
|
||||
(commands/load-commands commands/register))))
|
||||
|
||||
(defn load-chats-from-rpc
|
||||
[cofx from to]
|
||||
(data-store.chats/fetch-chats-rpc cofx {:from 0
|
||||
:to 10
|
||||
:on-success
|
||||
#(re-frame/dispatch
|
||||
[:chats-list/load-success %])}))
|
||||
(fx/defn initialize-chats
|
||||
"Initialize persisted chats on startup"
|
||||
[cofx
|
||||
{:keys [from to] :or {from 0 to nil}}]
|
||||
(if config/use-status-go-protocol?
|
||||
(load-chats-from-rpc cofx)
|
||||
(initialize-chats-legacy cofx from to)))
|
||||
(load-chats-from-rpc cofx from -1))
|
||||
|
||||
(defn load-more-messages
|
||||
"Loads more messages for current chat"
|
||||
|
@ -19,6 +19,8 @@
|
||||
[{:keys [db] :as cofx} chat-id removed-chat-messages]
|
||||
(let [removed-messages-ids (map :message-id removed-chat-messages)
|
||||
removed-unseen-count (count (remove :seen removed-chat-messages))
|
||||
unviewed-messages-count (- (get-in db [:chats chat-id :unviewed-messages-count])
|
||||
removed-unseen-count)
|
||||
db (-> db
|
||||
;; remove messages
|
||||
(update-in [:chats chat-id :messages]
|
||||
@ -32,8 +34,9 @@
|
||||
(chat.models/upsert-chat
|
||||
{:chat-id chat-id
|
||||
:unviewed-messages-count
|
||||
(- (get-in db [:chats chat-id :unviewed-messages-count])
|
||||
removed-unseen-count)})
|
||||
(if (pos? unviewed-messages-count)
|
||||
unviewed-messages-count
|
||||
0)})
|
||||
;; recompute message group
|
||||
(chat.models.loading/group-chat-messages
|
||||
chat-id
|
||||
|
@ -2,7 +2,9 @@
|
||||
(:require [goog.object :as object]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.data-store.messages :as messages]
|
||||
[status-im.utils.fx :as fx]
|
||||
[status-im.data-store.realm.core :as core]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[taoensso.timbre :as log]
|
||||
[status-im.utils.clocks :as utils.clocks]
|
||||
@ -16,6 +18,10 @@
|
||||
(and (coll? v)
|
||||
(empty? v)))) e)))
|
||||
|
||||
(def one-to-one-chat-type 1)
|
||||
(def public-chat-type 2)
|
||||
(def private-group-chat-type 3)
|
||||
|
||||
(defn- event->string
|
||||
"Transform an event in an a vector with keys in alphabetical order, to compute
|
||||
a predictable id"
|
||||
@ -31,121 +37,124 @@
|
||||
|
||||
(defn marshal-membership-updates [updates]
|
||||
(mapcat (fn [{:keys [signature events from]}]
|
||||
(map #(assoc %
|
||||
:id (event-id %)
|
||||
:signature signature
|
||||
:from from) events)) updates))
|
||||
(map #(-> %
|
||||
(assoc
|
||||
:clockValue (:clock-value %)
|
||||
:id (event-id %)
|
||||
:signature signature
|
||||
:from from)
|
||||
(dissoc :clock-value)) events)) updates))
|
||||
|
||||
(defn unmarshal-membership-updates [chat-id updates]
|
||||
(->> updates
|
||||
vals
|
||||
(group-by :signature)
|
||||
(map (fn [[signature events]]
|
||||
{:events (map #(-> (dissoc % :signature :from :id)
|
||||
{:events (map #(-> %
|
||||
(assoc :clock-value (:clockValue %))
|
||||
(dissoc :signature :from :id :clockValue)
|
||||
remove-empty-vals) events)
|
||||
:from (-> events first :from)
|
||||
:signature signature
|
||||
:chat-id chat-id}))))
|
||||
|
||||
(defn- normalize-chat [{:keys [chat-id] :as chat}]
|
||||
(defn type->rpc [{:keys [public? group-chat] :as chat}]
|
||||
(assoc chat :chatType (cond
|
||||
public? public-chat-type
|
||||
group-chat private-group-chat-type
|
||||
:else one-to-one-chat-type)))
|
||||
|
||||
(defn rpc->type [{:keys [chatType] :as chat}]
|
||||
(cond
|
||||
(= public-chat-type chatType) (assoc chat :public? true :group-chat true)
|
||||
(= private-group-chat-type chatType) (assoc chat :public? false :group-chat true)
|
||||
:else (assoc chat :public? false :group-chat false)))
|
||||
|
||||
(defn- marshal-members [{:keys [admins contacts members-joined chatType] :as chat}]
|
||||
(cond-> chat
|
||||
(= chatType private-group-chat-type)
|
||||
(assoc :members (map #(hash-map :id %
|
||||
:admin (boolean (admins %))
|
||||
:joined (boolean (members-joined %))) contacts))
|
||||
:always
|
||||
(dissoc :admins :contacts :members-joined)))
|
||||
|
||||
(defn- unmarshal-members [{:keys [members chatType] :as chat}]
|
||||
(cond
|
||||
(= public-chat-type chatType) (assoc chat
|
||||
:contacts #{}
|
||||
:admins #{}
|
||||
:members-joined #{})
|
||||
(= private-group-chat-type chatType) (merge chat
|
||||
(reduce (fn [acc member]
|
||||
(cond-> acc
|
||||
(:admin member)
|
||||
(update :admins conj (:id member))
|
||||
(:joined member)
|
||||
(update :members-joined conj (:id member))
|
||||
:always
|
||||
(update :contacts conj (:id member))))
|
||||
{:admins #{}
|
||||
:members-joined #{}
|
||||
:contacts #{}}
|
||||
members))
|
||||
:else
|
||||
(assoc chat
|
||||
:contacts #{(:id chat)}
|
||||
:admins #{}
|
||||
:members-joined #{})))
|
||||
|
||||
(defn- ->rpc [chat]
|
||||
(-> chat
|
||||
(update :admins #(into #{} %))
|
||||
(update :contacts #(into #{} %))
|
||||
(update :members-joined #(into #{} %))
|
||||
(update :tags #(into #{} %))
|
||||
(update :membership-updates (partial unmarshal-membership-updates chat-id))
|
||||
type->rpc
|
||||
marshal-members
|
||||
(update :membership-updates marshal-membership-updates)
|
||||
(utils/update-if-present :last-message-content messages/prepare-content)
|
||||
(clojure.set/rename-keys {:chat-id :id
|
||||
:membership-updates :membershipUpdates
|
||||
:unviewed-messages-count :unviewedMessagesCount
|
||||
:last-message-content :lastMessageContent
|
||||
:last-message-content-type :lastMessageContentType
|
||||
:deleted-at-clock-value :deletedAtClockValue
|
||||
:is-active :active
|
||||
:last-clock-value :lastClockValue})
|
||||
(dissoc :referenced-messages :message-groups :gaps-loaded? :pagination-info
|
||||
:public? :group-chat :messages
|
||||
:might-have-join-time-messages?
|
||||
:group-chat-local-version :loaded-unviewed-messages-ids
|
||||
:messages-initialized? :contacts :admins :members-joined)))
|
||||
|
||||
(defn- <-rpc [chat]
|
||||
(-> chat
|
||||
rpc->type
|
||||
unmarshal-members
|
||||
(clojure.set/rename-keys {:id :chat-id
|
||||
:membershipUpdates :membership-updates
|
||||
:unviewedMessagesCount :unviewed-messages-count
|
||||
:lastMessageContent :last-message-content
|
||||
:lastMessageContentType :last-message-content-type
|
||||
:deletedAtClockValue :deleted-at-clock-value
|
||||
:active :is-active
|
||||
:lastClockValue :last-clock-value})
|
||||
(update :membership-updates (partial unmarshal-membership-updates (:id chat)))
|
||||
(update :last-message-content utils/safe-read-message-content)
|
||||
(update :last-clock-value utils.clocks/safe-timestamp)
|
||||
(update :last-message-content utils/safe-read-message-content)))
|
||||
(assoc :group-chat-local-version 1) ;; TODO(cammellos): this can be removed
|
||||
(dissoc :chatType :members)))
|
||||
|
||||
(re-frame/reg-cofx
|
||||
:data-store/all-chats
|
||||
(fn [cofx _]
|
||||
(assoc cofx :get-all-stored-chats
|
||||
(fn [from to]
|
||||
(map normalize-chat
|
||||
(-> @core/account-realm
|
||||
(core/get-all :chat)
|
||||
(core/sorted :timestamp :desc)
|
||||
(core/page from to)
|
||||
(core/all-clj :chat)))))))
|
||||
(fx/defn save-chat-rpc [cofx {:keys [chat-id] :as chat}]
|
||||
(json-rpc/call {:method "shhext_saveChat"
|
||||
:params [(->rpc chat)]
|
||||
:on-success #(log/debug "saved chat" chat-id "successfuly")
|
||||
:on-failure #(log/error "failed to save chat" chat-id %)}))
|
||||
|
||||
(defn save-chat-tx
|
||||
"Returns tx function for saving chat"
|
||||
[chat]
|
||||
(fn [realm]
|
||||
(log/debug "saving chat" chat)
|
||||
(core/create
|
||||
realm
|
||||
:chat
|
||||
(-> chat
|
||||
(update :membership-updates marshal-membership-updates)
|
||||
(utils/update-if-present :last-message-content messages/prepare-content))
|
||||
true)))
|
||||
(fx/defn fetch-chats-rpc [cofx {:keys [from to on-success]}]
|
||||
(json-rpc/call {:method "shhext_chats"
|
||||
:params [from to]
|
||||
:on-success #(on-success (map <-rpc %))
|
||||
:on-failure #(log/error "failed to fetch chats" from to %)}))
|
||||
|
||||
;; Only used in debug mode
|
||||
(defn delete-chat-tx
|
||||
"Returns tx function for hard deleting the chat"
|
||||
[chat-id]
|
||||
(fn [realm]
|
||||
(core/delete realm (core/get-by-field realm :chat :chat chat-id))))
|
||||
|
||||
(defn- get-chat-by-id [chat-id realm]
|
||||
(.objectForPrimaryKey realm "chat" chat-id))
|
||||
|
||||
(defn clear-history-tx
|
||||
"Returns tx function for clearing the history of chat"
|
||||
[chat-id deleted-at-clock-value]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)]
|
||||
(doto chat
|
||||
(aset "last-message-content" nil)
|
||||
(aset "last-message-content-type" nil)
|
||||
(aset "deleted-at-clock-value" deleted-at-clock-value)))))
|
||||
|
||||
(defn deactivate-chat-tx
|
||||
"Returns tx function for deactivating chat"
|
||||
[chat-id now]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)]
|
||||
(doto chat
|
||||
(aset "is-active" false)))))
|
||||
|
||||
(defn add-chat-contacts-tx
|
||||
"Returns tx function for adding chat contacts"
|
||||
[chat-id contacts]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)
|
||||
existing-contacts (object/get chat "contacts")]
|
||||
(aset chat "contacts"
|
||||
(clj->js (into #{} (concat contacts
|
||||
(core/list->clj existing-contacts))))))))
|
||||
|
||||
(defn remove-chat-contacts-tx
|
||||
"Returns tx function for removing chat contacts"
|
||||
[chat-id contacts]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)
|
||||
existing-contacts (object/get chat "contacts")]
|
||||
(aset chat "contacts"
|
||||
(clj->js (remove (into #{} contacts)
|
||||
(core/list->clj existing-contacts)))))))
|
||||
|
||||
(defn add-chat-tag-tx
|
||||
"Returns tx function for adding chat contacts"
|
||||
[chat-id tag]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)
|
||||
existing-tags (object/get chat "tags")]
|
||||
(aset chat "tags"
|
||||
(clj->js (into #{} (concat tag
|
||||
(core/list->clj existing-tags))))))))
|
||||
|
||||
(defn remove-chat-tag-tx
|
||||
"Returns tx function for removing chat contacts"
|
||||
[chat-id tag]
|
||||
(fn [realm]
|
||||
(let [chat (get-chat-by-id chat-id realm)
|
||||
existing-tags (object/get chat "tags")]
|
||||
(aset chat "tags"
|
||||
(clj->js (remove (into #{} tag)
|
||||
(core/list->clj existing-tags)))))))
|
||||
(defn delete-chat-rpc [chat-id chat-type]
|
||||
(json-rpc/call {:method "shhext_deleteChat"
|
||||
:params [chat-id chat-type]
|
||||
:on-success #(log/debug "deleteed chat" chat-id chat-type)
|
||||
:on-failure #(log/error "failed to delete chat" chat-id chat-type %)}))
|
||||
|
@ -1,5 +1,8 @@
|
||||
(ns status-im.data-store.contacts
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.utils.fx :as fx]
|
||||
[status-im.data-store.chats :as data-store.chats]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[taoensso.timbre :as log]
|
||||
[status-im.data-store.realm.core :as core]))
|
||||
|
||||
@ -69,9 +72,7 @@
|
||||
(when-let [user-messages
|
||||
(get-messages-by-messages-ids messages-ids)]
|
||||
(core/delete realm user-messages))
|
||||
(when-let [chat
|
||||
(get-chat public-key)]
|
||||
(core/delete realm chat))))
|
||||
(data-store.chats/delete-chat-rpc public-key data-store.chats/one-to-one-chat-type)))
|
||||
|
||||
(defn delete-contact-tx
|
||||
"Returns tx function for deleting contact"
|
||||
|
@ -32,6 +32,11 @@
|
||||
"shhext_loadFilters" {}
|
||||
"shhext_loadFilter" {}
|
||||
"shhext_removeFilters" {}
|
||||
"shhext_chats" {}
|
||||
"shhext_saveChat" {}
|
||||
"shhext_contacts" {}
|
||||
"shhext_deleteChat" {}
|
||||
"shhext_saveContact" {}
|
||||
"status_joinPublicChat" {}
|
||||
"status_chats" {}
|
||||
"status_startOneOnOneChat" {}
|
||||
|
@ -114,16 +114,6 @@
|
||||
(fn [cofx [_ encryption-key error]]
|
||||
(init/handle-init-store-error cofx encryption-key)))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:init-rest-of-chats
|
||||
[(re-frame/inject-cofx :web3/get-web3)
|
||||
(re-frame/inject-cofx :data-store/all-chats)]
|
||||
(fn [{:keys [db] :as cofx} [_]]
|
||||
(log/debug "PERF" :init-rest-of-chats (.now js/Date))
|
||||
(fx/merge cofx
|
||||
{:db (assoc db :chats/loading? false)}
|
||||
(chat.loading/initialize-chats {:from 10}))))
|
||||
|
||||
(defn multiaccount-change-success
|
||||
[{:keys [db] :as cofx} [_ address nodes]]
|
||||
(let [{:node/keys [status]} db]
|
||||
@ -135,15 +125,13 @@
|
||||
(multiaccounts.login/login)
|
||||
(node/initialize (get-in db [:multiaccounts/login :address])))
|
||||
(init/initialize-multiaccount address)
|
||||
(mailserver/initialize-ranges)
|
||||
(chat.loading/initialize-chats {:to 10}))))
|
||||
(mailserver/initialize-ranges))))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:init.callback/multiaccount-change-success
|
||||
[(re-frame/inject-cofx :web3/get-web3)
|
||||
(re-frame/inject-cofx :data-store/get-all-contacts)
|
||||
(re-frame/inject-cofx :data-store/get-all-installations)
|
||||
(re-frame/inject-cofx :data-store/all-chats)
|
||||
(re-frame/inject-cofx :data-store/all-chat-requests-ranges)]
|
||||
multiaccount-change-success)
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
[clojure.string :as string]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.multiaccounts.model :as multiaccounts.model]
|
||||
[status-im.pairing.core :as pairing]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
[status-im.chat.models :as models.chat]
|
||||
[status-im.chat.models.message :as models.message]
|
||||
[status-im.contact.core :as models.contact]
|
||||
@ -125,7 +125,7 @@
|
||||
members-allowed (filter
|
||||
(fn [pk]
|
||||
(if (= pk current-public-key)
|
||||
(pairing/has-paired-installations? cofx)
|
||||
(pairing.utils/has-paired-installations? cofx)
|
||||
true))
|
||||
members)
|
||||
destinations (map (fn [member]
|
||||
|
@ -5,6 +5,7 @@
|
||||
[status-im.data-store.core :as data-store]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.ethereum.subscriptions :as ethereum.subscriptions]
|
||||
[status-im.chat.models.loading :as chat.loading]
|
||||
[status-im.ethereum.transactions.core :as transactions]
|
||||
[status-im.fleet.core :as fleet]
|
||||
[status-im.i18n :as i18n]
|
||||
@ -207,6 +208,7 @@
|
||||
(tribute-to-talk/init)
|
||||
(mobile-network/on-network-status-change)
|
||||
(protocol/initialize-protocol)
|
||||
(chat.loading/initialize-chats {:to -1})
|
||||
(universal-links/process-stored-event)
|
||||
(chaos-mode/check-chaos-mode)
|
||||
(finish-keycard-setup)
|
||||
|
@ -3,6 +3,7 @@
|
||||
[clojure.string :as string]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.utils.fx :as fx]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.contact.device-info :as device-info]
|
||||
[status-im.contact.db :as contact.db]
|
||||
@ -79,12 +80,6 @@
|
||||
{:transport/confirm-messages-processed [{:web3 (:web3 db)
|
||||
:js-obj raw-message}]})
|
||||
|
||||
(defn has-paired-installations? [cofx]
|
||||
(->>
|
||||
(get-in cofx [:db :pairing/installations])
|
||||
vals
|
||||
(some :enabled?)))
|
||||
|
||||
(defn send-pair-installation [cofx payload]
|
||||
(let [{:keys [web3]} (:db cofx)
|
||||
current-public-key (multiaccounts.model/current-public-key cofx)]
|
||||
@ -303,7 +298,7 @@
|
||||
:payload payload}]}))
|
||||
|
||||
(fx/defn send-installation-message-fx [cofx payload]
|
||||
(when (has-paired-installations? cofx)
|
||||
(when (pairing.utils/has-paired-installations? cofx)
|
||||
(protocol/send payload nil cofx)))
|
||||
|
||||
(fx/defn sync-public-chat [cofx chat-id]
|
||||
|
@ -80,7 +80,6 @@
|
||||
"discovery.summary" (summary cofx event)
|
||||
"subscriptions.data" (ethereum.subscriptions/handle-signal cofx event)
|
||||
"subscriptions.error" (ethereum.subscriptions/handle-error cofx event)
|
||||
"status.chats.did-change" (chat.loading/load-chats-from-rpc cofx)
|
||||
"whisper.filter.added" (transport.filters/handle-negotiated-filter cofx event)
|
||||
"messages.new" (transport.message/receive-messages cofx event)
|
||||
"wallet" (ethereum.subscriptions/new-wallet-event cofx event)
|
||||
|
@ -40,7 +40,6 @@
|
||||
[{:keys [db web3 all-installations] :as cofx}]
|
||||
(fx/merge cofx
|
||||
(fetch-node-info-fx)
|
||||
(transport.filters/load-filters)
|
||||
(pairing/init all-installations)
|
||||
(publisher/start-fx)
|
||||
(mailserver/connect-to-mailserver)
|
||||
|
@ -5,6 +5,7 @@
|
||||
[status-im.constants :as constants]
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[status-im.transport.db :as transport.db]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
[status-im.data-store.transport :as transport-store]
|
||||
[status-im.transport.utils :as transport.utils]
|
||||
[status-im.tribute-to-talk.whitelist :as whitelist]
|
||||
@ -12,12 +13,6 @@
|
||||
[status-im.utils.fx :as fx]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn has-paired-installations? [cofx]
|
||||
(->>
|
||||
(get-in cofx [:db :pairing/installations])
|
||||
vals
|
||||
(some :enabled?)))
|
||||
|
||||
(defn discovery-topic-hash [] (transport.utils/get-topic constants/contact-discovery))
|
||||
|
||||
(defprotocol StatusMessage
|
||||
@ -107,7 +102,7 @@
|
||||
|
||||
:user-message
|
||||
(fx/merge cofx
|
||||
(when (has-paired-installations? cofx)
|
||||
(when (pairing.utils/has-paired-installations? cofx)
|
||||
(send-direct-message current-public-key nil this))
|
||||
(send-with-pubkey params)))))
|
||||
(receive [this chat-id signature timestamp cofx]
|
||||
|
@ -150,11 +150,6 @@
|
||||
(views/letsubs [search-filter [:search/filter]
|
||||
logging-in? [:multiaccounts/login]
|
||||
{:keys [all-home-items chats]} [:home-items]]
|
||||
{:component-did-mount
|
||||
(fn [this]
|
||||
(let [[_ loading?] (.. this -props -argv)]
|
||||
(when loading?
|
||||
(re-frame/dispatch [:init-rest-of-chats]))))}
|
||||
[react/view {:style styles/chat-list-view}
|
||||
[react/view {:style styles/chat-list-header}
|
||||
[search-input search-filter]
|
||||
|
@ -16,38 +16,3 @@
|
||||
(assoc :current-chat-id chat-id))}
|
||||
(navigation/navigate-to-cofx :group-chat-profile nil))))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:add-new-group-chat-participants
|
||||
[(re-frame/inject-cofx :random-id-generator)]
|
||||
(fn [{{:keys [current-chat-id selected-participants] :as db} :db
|
||||
now :now random-id-generator :random-id-generator :as cofx} _]
|
||||
(let [message-id (random-id-generator)
|
||||
participants (concat (get-in db [:chats current-chat-id :contacts]) selected-participants)
|
||||
contacts (:contacts/contacts db)
|
||||
added-participants-names (map #(get-in contacts [% :name]) selected-participants)]
|
||||
(fx/merge cofx
|
||||
{:db (-> db
|
||||
(assoc-in [:chats current-chat-id :contacts] participants)
|
||||
(assoc :selected-participants #{}))
|
||||
:data-store/tx [(chats-store/add-chat-contacts-tx current-chat-id selected-participants)]}
|
||||
#_(models.message/receive
|
||||
(models.message/system-message current-chat-id message-id now
|
||||
(str "You've added " (apply str (interpose ", " added-participants-names)))))
|
||||
#_(transport/send (protocol/GroupAdminUpdate. nil participants current-chat-id) current-chat-id)))))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:remove-group-chat-participants
|
||||
[(re-frame/inject-cofx :random-id-generator)]
|
||||
(fn [{{:keys [current-chat-id] :as db} :db now :now random-id-generator :random-id-generator :as cofx}
|
||||
[_ removed-participants]]
|
||||
(let [message-id (random-id-generator)
|
||||
participants (remove removed-participants (get-in db [:chats current-chat-id :contacts]))
|
||||
contacts (:contacts/contacts db)
|
||||
removed-participants-names (map #(get-in contacts [% :name]) removed-participants)]
|
||||
(fx/merge cofx
|
||||
{:db (assoc-in db [:chats current-chat-id :contacts] participants)
|
||||
:data-store/tx [(chats-store/remove-chat-contacts-tx current-chat-id removed-participants)]}
|
||||
#_(models.message/receive
|
||||
(models.message/system-message current-chat-id message-id now
|
||||
(str "You've removed " (apply str (interpose ", " removed-participants-names)))))
|
||||
#_(transport/send (protocol/GroupAdminUpdate. nil participants current-chat-id) current-chat-id)))))
|
||||
|
@ -126,9 +126,6 @@
|
||||
{:keys [search-filter chats all-home-items]} [:home-items]
|
||||
window-width [:dimensions/window-width]
|
||||
two-pane-ui-enabled? [:two-pane-ui-enabled?]]
|
||||
{:component-did-mount (fn [this]
|
||||
(let [[_ loading?] (.. this -props -argv)]
|
||||
(when loading? (utils/set-timeout #(re-frame/dispatch [:init-rest-of-chats]) 100))))}
|
||||
(let [home-width (if (> window-width constants/two-pane-min-width)
|
||||
(max constants/left-pane-min-width (/ window-width 3))
|
||||
window-width)]
|
||||
|
11
src/status_im/utils/pairing.cljs
Normal file
11
src/status_im/utils/pairing.cljs
Normal file
@ -0,0 +1,11 @@
|
||||
(ns ^{:doc "Pairing utils"}
|
||||
status-im.utils.pairing)
|
||||
|
||||
(defn has-paired-installations? [cofx]
|
||||
(let [our-installation-id (get-in cofx [:db :multiaccount :installation-id])]
|
||||
(->>
|
||||
(get-in cofx [:db :pairing/installations])
|
||||
vals
|
||||
(some (fn [{:keys [enabled? installation-id]}]
|
||||
(and (not= installation-id our-installation-id)
|
||||
enabled?))))))
|
@ -2,7 +2,7 @@
|
||||
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
|
||||
"owner": "status-im",
|
||||
"repo": "status-go",
|
||||
"version": "9de77b21b22aeca3c71050ed4f7809f500e63191",
|
||||
"commit-sha1": "9de77b21b22aeca3c71050ed4f7809f500e63191",
|
||||
"src-sha256": "1wpyijxqcq1c1ra6vp6sr8zg91r0awpdx5ypgdrvhlnl3mba4k1g"
|
||||
"version": "a3a413b5d98f227ff541ad9527c8d24e9c9ce653",
|
||||
"commit-sha1": "a3a413b5d98f227ff541ad9527c8d24e9c9ce653",
|
||||
"src-sha256": "07sgvpmmjiigga73gymbq1ifm047frmrxn3dr26ihw50p0kp0fwh"
|
||||
}
|
||||
|
@ -13,8 +13,7 @@
|
||||
:db {:contacts/contacts {chat-id
|
||||
{:name contact-name}}}}
|
||||
response (chat/upsert-chat cofx chat-props)
|
||||
actual-chat (get-in response [:db :chats chat-id])
|
||||
store-chat-fx (:data-store/tx response)]
|
||||
actual-chat (get-in response [:db :chats chat-id])]
|
||||
(testing "it adds the chat to the chats collection"
|
||||
(is actual-chat))
|
||||
(testing "it adds the extra props"
|
||||
@ -26,9 +25,7 @@
|
||||
(testing "it sets the timestamp"
|
||||
(is (= "now" (:timestamp actual-chat))))
|
||||
(testing "it adds the contact-id to the contact field"
|
||||
(is (= chat-id (-> actual-chat :contacts first))))
|
||||
(testing "it adds the fx to store a chat"
|
||||
(is store-chat-fx))))
|
||||
(is (= chat-id (-> actual-chat :contacts first))))))
|
||||
(testing "upserting an existing chat"
|
||||
(let [chat-id "some-chat-id"
|
||||
chat-props {:chat-id chat-id
|
||||
@ -37,24 +34,19 @@
|
||||
cofx {:db {:chats {chat-id {:is-active true
|
||||
:name "old-name"}}}}
|
||||
response (chat/upsert-chat cofx chat-props)
|
||||
actual-chat (get-in response [:db :chats chat-id])
|
||||
store-chat-fx (:data-store/tx response)]
|
||||
actual-chat (get-in response [:db :chats chat-id])]
|
||||
(testing "it adds the chat to the chats collection"
|
||||
(is actual-chat))
|
||||
(testing "it adds the extra props"
|
||||
(is (= "some" (:extra-prop actual-chat))))
|
||||
(testing "it updates existins props"
|
||||
(is (= "new-name" (:name actual-chat))))
|
||||
(testing "it adds the fx to store a chat"
|
||||
(is store-chat-fx)))))
|
||||
(is (= "new-name" (:name actual-chat)))))))
|
||||
|
||||
(deftest add-public-chat
|
||||
(let [topic "topic"
|
||||
fx (chat/add-public-chat {:db {}} topic)
|
||||
store-fx (:data-store/tx fx)
|
||||
chat (get-in fx [:db :chats topic])]
|
||||
(testing "it saves the chat in the database"
|
||||
(is store-fx))
|
||||
(testing "it sets the name"
|
||||
(is (= topic (:name chat))))
|
||||
(testing "it sets the participants"
|
||||
@ -104,7 +96,7 @@
|
||||
(testing "it adds the relevant transactions for realm"
|
||||
(let [actual (chat/clear-history cofx chat-id)]
|
||||
(is (:data-store/tx actual))
|
||||
(is (= 2 (count (:data-store/tx actual))))))))
|
||||
(is (= 1 (count (:data-store/tx actual))))))))
|
||||
|
||||
(deftest remove-chat-test
|
||||
(let [chat-id "1"
|
||||
@ -142,7 +134,7 @@
|
||||
(testing "it adds the relevant transactions for realm"
|
||||
(let [actual (chat/remove-chat cofx chat-id)]
|
||||
(is (:data-store/tx actual))
|
||||
(is (= 6 (count (:data-store/tx actual))))))))
|
||||
(is (= 4 (count (:data-store/tx actual))))))))
|
||||
|
||||
(deftest multi-user-chat?
|
||||
(let [chat-id "1"]
|
||||
@ -183,7 +175,7 @@
|
||||
me (get-in test-db [:multiaccount :public-key])]
|
||||
(is (= '(true true true)
|
||||
(map (comp :seen second) (get-in fx [:db :chats "status" :messages]))))
|
||||
(is (= 2 (count (:data-store/tx fx))))
|
||||
(is (= 1 (count (:data-store/tx fx))))
|
||||
;; for public chats, no confirmation is sent out
|
||||
(is (= nil (:shh/post fx)))))
|
||||
|
||||
|
@ -3,38 +3,140 @@
|
||||
[status-im.utils.random :as utils.random]
|
||||
[status-im.data-store.chats :as chats]))
|
||||
|
||||
(deftest ->to-rpc
|
||||
(let [chat {:referenced-messages []
|
||||
:public? false
|
||||
:group-chat true
|
||||
:message-groups {}
|
||||
:color "color"
|
||||
:contacts #{"a" "b" "c" "d"}
|
||||
:last-clock-value 10
|
||||
:admins #{"a" "b"}
|
||||
:members-joined #{"a" "c"}
|
||||
:name "name"
|
||||
:membership-updates [{:chat-id "chat-id"
|
||||
:from "a"
|
||||
:signature "b"
|
||||
:events [{:type "chat-created"
|
||||
:name "test"
|
||||
:clock-value 1}
|
||||
{:type "members-added"
|
||||
:clock-value 2
|
||||
:members ["a" "b"]}]}]
|
||||
:gaps-loaded? true
|
||||
:unviewed-messages-count 2
|
||||
:is-active true
|
||||
:messages {}
|
||||
:pagination-info {}
|
||||
:last-message-content "content"
|
||||
:last-message-content-type "type"
|
||||
:group-chat-local-version 1
|
||||
:chat-id "chat-id"
|
||||
:loaded-unviewed-messages-ids []
|
||||
:timestamp 2
|
||||
:messages-initialized? true}
|
||||
expected-chat {:id "chat-id"
|
||||
:color "color"
|
||||
:name "name"
|
||||
:chatType 3
|
||||
:lastMessageContent "content"
|
||||
:lastMessageContentType "type"
|
||||
:members #{{:id "a"
|
||||
:admin true
|
||||
:joined true}
|
||||
{:id "b"
|
||||
:admin true
|
||||
:joined false}
|
||||
{:id "c"
|
||||
:admin false
|
||||
:joined true}
|
||||
{:id "d"
|
||||
:admin false
|
||||
:joined false}}
|
||||
:lastClockValue 10
|
||||
:membershipUpdates #{{:type "chat-created"
|
||||
:name "test"
|
||||
:clockValue 1
|
||||
:id "0xcdf4a63e0c98d0018cf532b3a48350bb80e292cc46249e3f876aaa65eb97a231"
|
||||
:signature "b"
|
||||
:from "a"}
|
||||
{:type "members-added"
|
||||
:clockValue 2
|
||||
:members ["a" "b"]
|
||||
:id "0x1c34d6b4d022c432b7eb6b645e095791af0c6bdb626db7d705e6db0d7cd74b56"
|
||||
:signature "b"
|
||||
:from "a"}}
|
||||
:unviewedMessagesCount 2
|
||||
:active true
|
||||
:timestamp 2}]
|
||||
(testing "marshaling chat"
|
||||
(is (= expected-chat (-> (chats/->rpc chat)
|
||||
(update :members #(into #{} %))
|
||||
(update :membershipUpdates #(into #{} %))))))))
|
||||
|
||||
(deftest normalize-chat-test
|
||||
(testing "admins & contacts"
|
||||
(is (= {:admins #{4}
|
||||
:contacts #{2}
|
||||
:tags #{}
|
||||
:membership-updates []
|
||||
:members-joined #{}
|
||||
:last-message-content {:foo "bar"}
|
||||
:last-clock-value nil}
|
||||
(chats/normalize-chat
|
||||
{:admins [4]
|
||||
:contacts [2]
|
||||
:last-message-content "{:foo \"bar\"}"}))))
|
||||
(testing "membership-updates"
|
||||
(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)))))
|
||||
(let [chat {:id "chat-id"
|
||||
:color "color"
|
||||
:name "name"
|
||||
:chatType 3
|
||||
:members [{:id "a"
|
||||
:admin true
|
||||
:joined true}
|
||||
{:id "b"
|
||||
:admin true
|
||||
:joined false}
|
||||
{:id "c"
|
||||
:admin false
|
||||
:joined true}
|
||||
{:id "d"
|
||||
:admin false
|
||||
:joined false}]
|
||||
:lastClockValue 10
|
||||
:lastMessageContent "\"content\"" ;; goes through edn/read-string
|
||||
:lastMessageContentType "type"
|
||||
|
||||
:membershipUpdates [{:type "chat-created"
|
||||
:name "test"
|
||||
:clockValue 1
|
||||
:id "0xcdf4a63e0c98d0018cf532b3a48350bb80e292cc46249e3f876aaa65eb97a231"
|
||||
:signature "b"
|
||||
:from "a"}
|
||||
{:type "members-added"
|
||||
:clockValue 2
|
||||
:members ["a" "b"]
|
||||
:id "0x1c34d6b4d022c432b7eb6b645e095791af0c6bdb626db7d705e6db0d7cd74b56"
|
||||
:signature "b"
|
||||
:from "a"}]
|
||||
:unviewedMessagesCount 2
|
||||
:active true
|
||||
:timestamp 2}
|
||||
expected-chat {:public? false
|
||||
:group-chat true
|
||||
:color "color"
|
||||
:last-message-content "content"
|
||||
:last-message-content-type "type"
|
||||
|
||||
:contacts #{"a" "b" "c" "d"}
|
||||
:last-clock-value 10
|
||||
:admins #{"a" "b"}
|
||||
:members-joined #{"a" "c"}
|
||||
:name "name"
|
||||
:membership-updates [{:chat-id "chat-id"
|
||||
:from "a"
|
||||
:signature "b"
|
||||
:events [{:type "chat-created"
|
||||
:name "test"
|
||||
:clock-value 1}
|
||||
{:type "members-added"
|
||||
:clock-value 2
|
||||
:members ["a" "b"]}]}]
|
||||
:unviewed-messages-count 2
|
||||
:is-active true
|
||||
:group-chat-local-version 1
|
||||
:chat-id "chat-id"
|
||||
:timestamp 2}]
|
||||
(testing "from-rpc"
|
||||
(is (= expected-chat (chats/<-rpc chat))))))
|
||||
|
||||
(deftest marshal-membership-updates-test
|
||||
(let [raw-updates [{:chat-id "chat-id"
|
||||
@ -46,8 +148,8 @@
|
||||
: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"}}
|
||||
expected #{{:type "members-added" :clockValue 10 :from "id-2" :members [1 2] :signature "a" :id "0xb7690375de21da4890d2d5acca8b56e327d9eb75fd3b4bcceca4bf1679c2f830"}
|
||||
{:type "member-removed" :clockValue 11 :from "id-2" :member 1 :signature "a" :id "0x2a66f195abf6e6903c4245e372e1e2e6aea2b2c0a74ad03080a313e94197a64f"}
|
||||
{:type "chat-created" :clockValue 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.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.utils.clocks :as utils.clocks]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.group-chats.core :as group-chats]))
|
||||
@ -492,7 +493,8 @@
|
||||
:a "a-value"}]))))
|
||||
|
||||
(deftest remove-group-chat-test
|
||||
(with-redefs [utils.clocks/send inc]
|
||||
(with-redefs [json-rpc/call (constantly nil)
|
||||
utils.clocks/send inc]
|
||||
(let [cofx {:db {:chats {chat-id {:admins #{member-1 member-2}
|
||||
:name "chat-name"
|
||||
:chat-id chat-id
|
||||
|
@ -2,6 +2,7 @@
|
||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||
[status-im.transport.message.pairing :as transport.pairing]
|
||||
[status-im.utils.identicon :as identicon]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.pairing.core :as pairing]))
|
||||
|
||||
@ -308,11 +309,18 @@
|
||||
|
||||
(deftest has-paired-installations-test
|
||||
(testing "no paired devices"
|
||||
(is (not (pairing/has-paired-installations? {:db {:pairing/installations {"1" {}
|
||||
"2" {}}}}))))
|
||||
(is (not (pairing.utils/has-paired-installations? {:db {:multiaccount {:installation-id "1"}
|
||||
:pairing/installations {"1" {:installation-id "1"
|
||||
:enabled? true}
|
||||
"2" {:installation-id "2"}
|
||||
"3" {:installation-id "3"}}}}))))
|
||||
(testing "has paired devices"
|
||||
(is (pairing/has-paired-installations? {:db {:pairing/installations {"1" {}
|
||||
"2" {:enabled? true}}}}))))
|
||||
(is (pairing.utils/has-paired-installations? {:db {:pairing/installations {:multiaccount {:instllation-id "1"}
|
||||
"1" {:installation-id "1"
|
||||
:enabled? true}
|
||||
"2" {:installation-id "2"}
|
||||
"3" {:installation-id "3"
|
||||
:enabled? true}}}}))))
|
||||
|
||||
(deftest sort-installations
|
||||
(let [id "0"
|
||||
|
@ -38,8 +38,7 @@
|
||||
cofx {:db db
|
||||
:web3 :web3
|
||||
:all-contacts data/all-contacts
|
||||
:all-installations []
|
||||
:get-all-stored-chats data/get-chats}
|
||||
:all-installations []}
|
||||
efx (events/multiaccount-change-success cofx [nil "address"])
|
||||
new-db (:db efx)]
|
||||
(testing "Starting node."
|
||||
@ -52,8 +51,6 @@
|
||||
(is (= [:home nil] (efx :status-im.ui.screens.navigation/navigate-to))))
|
||||
(testing "Multiaccount selected."
|
||||
(is (contains? new-db :multiaccount)))
|
||||
(testing "Chats initialized."
|
||||
(is (= 3 (count (:chats new-db)))))
|
||||
(testing "Contacts initialized."
|
||||
(is (= 2 (count (:contacts/contacts new-db))))))))
|
||||
|
||||
|
@ -14,8 +14,6 @@
|
||||
:sym-key "sk3"}
|
||||
"4" {:topic "topic-4"}}
|
||||
:semaphores #{}}}]
|
||||
(testing "it loads the filters"
|
||||
(is (:filters/load-filters (transport/init-whisper cofx))))
|
||||
(testing "custom mailservers"
|
||||
(let [ms-1 {:id "1"
|
||||
:fleet :eth.beta
|
||||
|
Loading…
x
Reference in New Issue
Block a user