diff --git a/src/status_im/bots/events.cljs b/src/status_im/bots/events.cljs index 50c6a04192..d86682b9fb 100644 --- a/src/status_im/bots/events.cljs +++ b/src/status_im/bots/events.cljs @@ -22,11 +22,11 @@ :function :subscription :parameters {:name sub-name :subscriptions (subscription-values sub-params (get bot-db bot))} - :callback-events-creator (fn [jail-response] - [[::calculated-subscription - {:bot bot - :path [sub-name] - :result jail-response}]])})})) + :callback-event-creator (fn [jail-response] + [::calculated-subscription + {:bot bot + :path [sub-name] + :result jail-response}])})})) (defn set-in-bot-db "Associates value at specified path in bot-db and checks if there are any subscriptions diff --git a/src/status_im/chat/console.cljs b/src/status_im/chat/console.cljs index ca351c92ab..f4a685e712 100644 --- a/src/status_im/chat/console.cljs +++ b/src/status_im/chat/console.cljs @@ -26,7 +26,9 @@ :photo-path (str "contacts://" constants/console-chat-id) :contacts [{:identity constants/console-chat-id :text-color "#FFFFFF" - :background-color "#AB7967"}]}) + :background-color "#AB7967"}] + :last-to-clock-value 0 + :last-from-clock-value 0}) (def contact {:whisper-identity constants/console-chat-id diff --git a/src/status_im/chat/core.cljs b/src/status_im/chat/core.cljs new file mode 100644 index 0000000000..9ec424036e --- /dev/null +++ b/src/status_im/chat/core.cljs @@ -0,0 +1,12 @@ +(ns status-im.chat.core) + +;; Seen messages +(defn receive-seen + [chat-id sender message-ids {:keys [db]}] + (when-let [seen-messages-ids (-> (get-in db [:chats chat-id :messages]) + (select-keys message-ids) + keys)] + {:db (reduce (fn [new-db message-id] + (assoc-in new-db [:chats chat-id :messages message-id :user-statuses sender] :seen)) + db + seen-messages-ids)})) diff --git a/src/status_im/chat/events.cljs b/src/status_im/chat/events.cljs index a8f6806043..be03d88c64 100644 --- a/src/status_im/chat/events.cljs +++ b/src/status_im/chat/events.cljs @@ -1,19 +1,19 @@ (ns status-im.chat.events (:require [clojure.set :as set] - [cljs.core.async :as async] + [clojure.string :as string] [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.i18n :as i18n] - [status-im.protocol.core :as protocol] [status-im.chat.models :as models] [status-im.chat.console :as console] - [status-im.data-store.chats :as chats] - [status-im.data-store.messages :as messages] - [status-im.data-store.pending-messages :as pending-messages] + [status-im.chat.constants :as chat.constants] [status-im.ui.components.list-selection :as list-selection] [status-im.ui.screens.navigation :as navigation] - [status-im.utils.async :as utils.async] [status-im.utils.handlers :as handlers] + [status-im.transport.message.core :as transport] + [status-im.transport.message.v1.protocol :as protocol] + [status-im.transport.message.v1.public-chat :as public-chat] + [status-im.transport.message.v1.group-chat :as group-chat] status-im.chat.events.commands status-im.chat.events.requests status-im.chat.events.send-message @@ -22,88 +22,8 @@ status-im.chat.events.console status-im.chat.events.webview-bridge)) -;;;; Coeffects - -(re-frame/reg-cofx - :stored-unviewed-messages - (fn [cofx _] - (assoc cofx :stored-unviewed-messages - (messages/get-unviewed (-> cofx :db :current-public-key))))) - -(re-frame/reg-cofx - :get-stored-message - (fn [cofx _] - (assoc cofx :get-stored-message messages/get-by-id))) - -(re-frame/reg-cofx - :get-stored-messages - (fn [cofx _] - (assoc cofx :get-stored-messages messages/get-by-chat-id))) - -(re-frame/reg-cofx - :stored-message-ids - (fn [cofx _] - (assoc cofx :stored-message-ids (messages/get-stored-message-ids)))) - -(re-frame/reg-cofx - :all-stored-chats - (fn [cofx _] - (assoc cofx :all-stored-chats (chats/get-all)))) - -(re-frame/reg-cofx - :get-stored-chat - (fn [cofx _] - (assoc cofx :get-stored-chat chats/get-by-id))) - -(re-frame/reg-cofx - :inactive-chat-ids - (fn [cofx _] - (assoc cofx :inactive-chat-ids (chats/get-inactive-ids)))) - ;;;; Effects -(def ^:private realm-queue (utils.async/task-queue 2000)) - -(re-frame/reg-fx - :update-message - (fn [message] - (async/go (async/>! realm-queue #(messages/update-message message))))) - -(re-frame/reg-fx - :save-message - (fn [message] - (async/go (async/>! realm-queue #(messages/save message))))) - -(re-frame/reg-fx - :delete-messages - (fn [chat-id] - (async/go (async/>! realm-queue #(messages/delete-by-chat-id chat-id))))) - -(re-frame/reg-fx - :delete-pending-messages - (fn [chat-id] - (async/go (async/>! realm-queue #(pending-messages/delete-all-by-chat-id chat-id))))) - -(re-frame/reg-fx - :save-chat - (fn [chat] - (async/go (async/>! realm-queue #(chats/save chat))))) - -(re-frame/reg-fx - :deactivate-chat - (fn [chat-id] - (async/go (async/>! realm-queue #(chats/set-inactive chat-id))))) - -(re-frame/reg-fx - :delete-chat - (fn [chat-id] - (async/go (async/>! realm-queue #(chats/delete chat-id))))) - -(re-frame/reg-fx - :protocol-send-seen - (fn [params] - (protocol/send-seen! params))) - (re-frame/reg-fx :browse (fn [link] @@ -134,7 +54,7 @@ (handlers/register-handler-fx :load-more-messages - [(re-frame/inject-cofx :get-stored-messages)] + [(re-frame/inject-cofx :data-store/get-messages)] (fn [{{:keys [current-chat-id] :as db} :db get-stored-messages :get-stored-messages} _] (when-not (get-in db [:chats current-chat-id :all-loaded?]) (let [loaded-count (count (get-in db [:chats current-chat-id :messages])) @@ -151,16 +71,25 @@ (fn [db [{:keys [chat-id message-id]}]] (update-in db [:chats chat-id :messages message-id] assoc :appearing? false))) +(handlers/register-handler-fx + :update-message-status + [re-frame/trim-v] + (fn [{:keys [db]} [chat-id message-id user-id status]] + (let [msg-path [:chats chat-id :messages message-id] + new-db (update-in db (conj msg-path :user-statuses) assoc user-id status)] + {:db new-db + :data-store/update-message (-> (get-in new-db msg-path) (select-keys [:message-id :user-statuses]))}))) + (defn init-console-chat [{:keys [chats] :as db}] (if (chats constants/console-chat-id) {:db db} - {:db (-> db - (assoc :current-chat-id constants/console-chat-id) - (update :chats assoc constants/console-chat-id console/chat)) - :dispatch [:add-contacts [console/contact]] - :save-chat console/chat - :save-all-contacts [console/contact]})) + {:db (-> db + (assoc :current-chat-id constants/console-chat-id) + (update :chats assoc constants/console-chat-id console/chat)) + :dispatch [:add-contacts [console/contact]] + :data-store/save-chat console/chat + :data-store/save-contact console/contact})) (handlers/register-handler-fx :init-console-chat @@ -169,12 +98,12 @@ (handlers/register-handler-fx :initialize-chats - [(re-frame/inject-cofx :all-stored-chats) - (re-frame/inject-cofx :inactive-chat-ids) - (re-frame/inject-cofx :get-stored-messages) - (re-frame/inject-cofx :stored-unviewed-messages) - (re-frame/inject-cofx :stored-message-ids) - (re-frame/inject-cofx :get-stored-unanswered-requests)] + [(re-frame/inject-cofx :data-store/all-chats) + (re-frame/inject-cofx :data-store/inactive-chat-ids) + (re-frame/inject-cofx :data-store/get-messages) + (re-frame/inject-cofx :data-store/unviewed-messages) + (re-frame/inject-cofx :data-store/message-ids) + (re-frame/inject-cofx :data-store/get-unanswered-requests)] (fn [{:keys [db all-stored-chats inactive-chat-ids @@ -189,12 +118,12 @@ chats (reduce (fn [acc {:keys [chat-id] :as chat}] (let [chat-messages (index-messages (get-stored-messages chat-id))] (assoc acc chat-id - (assoc chat - :unviewed-messages (get stored-unviewed-messages chat-id) - :requests (get chat->message-id->request chat-id) - :messages chat-messages - :not-loaded-message-ids (set/difference (get stored-message-ids chat-id) - (-> chat-messages keys set)))))) + (assoc chat + :unviewed-messages (get stored-unviewed-messages chat-id) + :requests (get chat->message-id->request chat-id) + :messages chat-messages + :not-loaded-message-ids (set/difference (get stored-message-ids chat-id) + (-> chat-messages keys set)))))) {} all-stored-chats)] (-> db @@ -203,153 +132,213 @@ init-console-chat (update :dispatch-n conj [:load-default-contacts!]))))) -(handlers/register-handler-fx - :send-seen! - [re-frame/trim-v] - (fn [{:keys [db]} [{:keys [chat-id from me message-id]}]] - (let [{:keys [web3 chats] :contacts/keys [contacts]} db - {:keys [group-chat public? messages]} (get chats chat-id) - statuses (assoc (get-in messages [message-id :user-statuses]) me :seen)] - (cond-> {:db (-> db - (update-in [:chats chat-id :unviewed-messages] disj message-id) - (assoc-in [:chats chat-id :messages message-id :user-statuses] statuses)) - :update-message {:message-id message-id - :user-statuses statuses}} - ;; for public chats and 1-1 bot/dapp chats, it makes no sense to signalise `:seen` msg - (not (or public? (get-in contacts [chat-id :dapp?]))) - (assoc :protocol-send-seen {:web3 web3 - :message (cond-> {:from me - :to from - :message-id message-id} - group-chat (assoc :group-id chat-id))}))))) - (handlers/register-handler-fx :browse-link-from-message (fn [_ [_ link]] {:browse link})) -(defn preload-chat-data - "Takes coeffects map and chat-id, returns effects necessary when navigating to chat" - [{:keys [db]} chat-id] - (let [chat-loaded-event (get-in db [:chats chat-id :chat-loaded-event])] - (cond-> {:db (-> db - (assoc :current-chat-id chat-id) - (assoc-in [:chats chat-id :was-opened?] true) - (models/set-chat-ui-props {:validation-messages nil}) - (update-in [:chats chat-id] dissoc :chat-loaded-event))} +(defn- persist-seen-messages + [chat-id unseen-messages-ids {:keys [db]}] + {:data-store/update-messages (map (fn [message-id] + (-> (get-in db [:chats chat-id :messages message-id]) + (select-keys [:message-id :user-statuses]))) + unseen-messages-ids)}) - chat-loaded-event - (assoc :dispatch chat-loaded-event)))) +(defn- send-messages-seen [chat-id message-ids {:keys [db] :as cofx}] + (when (and (seq message-ids) + (not (models/bot-only-chat? db chat-id))) + (transport/send (protocol/map->MessagesSeen {:message-ids message-ids}) chat-id cofx))) + +;;TODO (yenda) find a more elegant solution for system messages +(defn- mark-messages-seen + [chat-id {:keys [db] :as cofx}] + (let [me (:current-public-key db) + messages-path [:chats chat-id :messages] + unseen-messages-ids (into #{} + (comp (filter (fn [[_ {:keys [from user-statuses outgoing]}]] + (and (not outgoing) + (not= constants/system from) + (not= (get user-statuses me) :seen)))) + (map first)) + (get-in db messages-path)) + unseen-system-messages-ids (into #{} + (comp (filter (fn [[_ {:keys [from user-statuses]}]] + (and (= constants/system from) + (not= (get user-statuses me) :seen)))) + (map first)) + (get-in db messages-path))] + (when (or (seq unseen-messages-ids) + (seq unseen-system-messages-ids)) + (handlers/merge-fx cofx + {:db (-> (reduce (fn [new-db message-id] + (assoc-in new-db (into messages-path [message-id :user-statuses me]) :seen)) + db + (into unseen-messages-ids unseen-system-messages-ids)) + (update-in [:chats chat-id :unviewed-messages] set/difference unseen-messages-ids unseen-system-messages-ids))} + (persist-seen-messages chat-id (into unseen-messages-ids unseen-system-messages-ids)) + (send-messages-seen chat-id unseen-messages-ids))))) + +(defn- fire-off-chat-loaded-event + [chat-id {:keys [db]}] + (when-let [event (get-in db [:chats chat-id :chat-loaded-event])] + {:db (update-in db [:chats chat-id] dissoc :chat-loaded-event) + :dispatch event})) + +(defn- preload-chat-data + "Takes chat-id and coeffects map, returns effects necessary when navigating to chat" + [chat-id {:keys [db] :as cofx}] + (handlers/merge-fx cofx + {:db (-> (assoc db :current-chat-id chat-id) + (models/set-chat-ui-props {:validation-messages nil}))} + (fire-off-chat-loaded-event chat-id) + (mark-messages-seen chat-id))) (handlers/register-handler-fx :add-chat-loaded-event - [(re-frame/inject-cofx :get-stored-chat) re-frame/trim-v] + [(re-frame/inject-cofx :data-store/get-chat) re-frame/trim-v] (fn [{:keys [db] :as cofx} [chat-id event]] (if (get (:chats db) chat-id) {:db (assoc-in db [:chats chat-id :chat-loaded-event] event)} - (-> (models/add-chat cofx chat-id) ; chat not created yet, we have to create it + (-> (models/add-chat chat-id cofx) ; chat not created yet, we have to create it (assoc-in [:db :chats chat-id :chat-loaded-event] event))))) ;; TODO(janherich): remove this unnecessary event in the future (only model function `add-chat` will stay) (handlers/register-handler-fx :add-chat - [(re-frame/inject-cofx :get-stored-chat) re-frame/trim-v] + [(re-frame/inject-cofx :data-store/get-chat) re-frame/trim-v] (fn [cofx [chat-id chat-props]] - (models/add-chat cofx chat-id chat-props))) + (models/add-chat chat-id chat-props cofx))) (defn- ensure-chat-exists "Takes chat-id and coeffects map and returns fx to create chat if it doesn't exist" - [chat-id cofx] + [chat-id {:keys [db] :as cofx}] (when-not (get-in cofx [:db :chats chat-id]) - (models/add-chat cofx chat-id))) + (models/add-chat chat-id cofx))) (defn- navigate-to-chat "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data" - [chat-id navigation-replace? cofx] - (let [nav-fn (if navigation-replace? - #(navigation/replace-view % :chat) - #(navigation/navigate-to % :chat))] - (-> (preload-chat-data cofx chat-id) - (update :db nav-fn)))) + [chat-id {:keys [navigation-replace?]} {:keys [db] :as cofx}] + (if navigation-replace? + (handlers/merge-fx cofx + (navigation/replace-view :chat) + (preload-chat-data chat-id)) + (handlers/merge-fx cofx + ;; TODO janherich - refactor `navigate-to` so it can be used with `merge-fx` macro + {:db (navigation/navigate-to db :chat)} + (preload-chat-data chat-id)))) (handlers/register-handler-fx :navigate-to-chat [re-frame/trim-v] - (fn [cofx [chat-id {:keys [navigation-replace?]}]] - (navigate-to-chat chat-id navigation-replace? cofx))) + (fn [cofx [chat-id opts]] + (navigate-to-chat chat-id opts cofx))) (defn start-chat "Start a chat, making sure it exists" - [chat-id navigation-replace? {:keys [db] :as cofx}] + [chat-id opts {:keys [db] :as cofx}] (when (not= (:current-public-key db) chat-id) ; don't allow to open chat with yourself - (handlers/merge-fx - cofx - (ensure-chat-exists chat-id) - (navigate-to-chat chat-id navigation-replace?)))) + (handlers/merge-fx cofx + (ensure-chat-exists chat-id) + (navigate-to-chat chat-id opts)))) (handlers/register-handler-fx :start-chat - [(re-frame/inject-cofx :get-stored-chat) re-frame/trim-v] - (fn [cofx [contact-id {:keys [navigation-replace?]}]] - (start-chat contact-id navigation-replace? cofx))) + [(re-frame/inject-cofx :data-store/get-chat) re-frame/trim-v] + (fn [cofx [contact-id opts]] + (start-chat contact-id opts cofx))) ;; TODO(janherich): remove this unnecessary event in the future (only model function `update-chat` will stay) (handlers/register-handler-fx :update-chat! [re-frame/trim-v] (fn [cofx [chat]] - (models/update-chat cofx chat))) - + (models/update-chat chat cofx))) (handlers/register-handler-fx :remove-chat [re-frame/trim-v] - (fn [{:keys [db]} [chat-id]] - (let [{:keys [chat-id group-chat debug?]} (get-in db [:chats chat-id])] - (cond-> {:db (-> db - (update :chats dissoc chat-id) - (update :deleted-chats (fnil conj #{}) chat-id)) - :delete-pending-messages chat-id} - (or group-chat debug?) - (assoc :delete-messages chat-id) - debug? - (assoc :delete-chat chat-id) - (not debug?) - (assoc :deactivate-chat chat-id))))) + (fn [cofx [chat-id]] + (models/remove-chat chat-id cofx))) (handlers/register-handler-fx - :delete-chat + :remove-chat-and-navigate-home [re-frame/trim-v] (fn [cofx [chat-id]] - (-> (models/remove-chat cofx chat-id) - (update :db navigation/replace-view :home)))) + (handlers/merge-fx cofx + (models/remove-chat chat-id) + (navigation/replace-view :home)))) (handlers/register-handler-fx - :delete-chat? + :remove-chat-and-navigate-home? [re-frame/trim-v] (fn [_ [chat-id group?]] {:show-confirmation {:title (i18n/label :t/delete-confirmation) :content (i18n/label (if group? :t/delete-group-chat-confirmation :t/delete-chat-confirmation)) :confirm-button-text (i18n/label :t/delete) - :on-accept #(re-frame/dispatch [:delete-chat chat-id])}})) - -(defn remove-chats [db chat-id] - (let [chat (get-in db [:chats chat-id])] - {:db (-> db - (update :chats dissoc chat-id) - (update :deleted-chats (fnil conj #{}) chat-id)) - :delete-chat chat - :delete-chat-messages chat})) + :on-accept #(re-frame/dispatch [:remove-chat-and-navigate-home chat-id])}})) (handlers/register-handler-fx - :remove-chat + :create-new-public-chat [re-frame/trim-v] - (fn [{:keys [db]} [chat-id]] - (remove-chats db chat-id))) + (fn [{:keys [db now] :as cofx} [topic]] + (if (get-in db [:chats topic]) + (handlers/merge-fx cofx + (navigation/navigate-to-clean :home) + (navigate-to-chat topic {})) + (handlers/merge-fx cofx + (models/add-public-chat topic) + (navigation/navigate-to-clean :home) + (navigate-to-chat topic {}) + (public-chat/join-public-chat topic))))) + +(defn- group-name-from-contacts [selected-contacts all-contacts username] + (->> selected-contacts + (map (comp :name (partial get all-contacts))) + (cons username) + (string/join ", "))) (handlers/register-handler-fx - :remove-chat-and-navigate-home + :create-new-group-chat-and-open + [re-frame/trim-v (re-frame/inject-cofx :random-id)] + (fn [{:keys [db now random-id] :as cofx} [group-name]] + (let [selected-contacts (:group/selected-contacts db) + chat-name (if-not (string/blank? group-name) + group-name + (group-name-from-contacts selected-contacts + (:contacts/contacts db) + (:username db)))] + (handlers/merge-fx cofx + {:db (assoc db :group/selected-contacts #{})} + (models/add-group-chat random-id chat-name (:current-public-key db) selected-contacts) + (navigation/navigate-to-clean :home) + (navigate-to-chat random-id {}) + (transport/send (group-chat/GroupAdminUpdate. chat-name selected-contacts) random-id))))) + +(defn- broadcast-leave [{:keys [public? chat-id]} cofx] + (when-not public? + (transport/send (group-chat/GroupLeave.) chat-id cofx))) + +(handlers/register-handler-fx + :leave-group-chat + ;; stop listening to group here + (fn [{{:keys [current-chat-id chats] :as db} :db :as cofx} _] + (handlers/merge-fx cofx + (models/remove-chat current-chat-id) + (navigation/replace-view :home) + (broadcast-leave (get chats current-chat-id))))) + +(handlers/register-handler-fx + :leave-group-chat? + (fn [_ _] + {:show-confirmation {:title (i18n/label :t/leave-confirmation) + :content (i18n/label :t/leave-group-chat-confirmation) + :confirm-button-text (i18n/label :t/leave) + :on-accept #(re-frame/dispatch [:leave-group-chat])}})) + +(handlers/register-handler-fx + :show-profile [re-frame/trim-v] - (fn [{:keys [db]} [chat-id]] - (merge (remove-chats db chat-id) - {:dispatch [:navigation-replace :home]}))) + (fn [{:keys [db] :as cofx} [identity]] + (handlers/merge-fx cofx + {:db (assoc db :contacts/identity identity)} + (navigation/navigate-forget :profile)))) diff --git a/src/status_im/chat/events/commands.cljs b/src/status_im/chat/events/commands.cljs index 669c8e1123..5c99ab203a 100644 --- a/src/status_im/chat/events/commands.cljs +++ b/src/status_im/chat/events/commands.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.events.commands (:require [re-frame.core :as re-frame] [taoensso.timbre :as log] + [status-im.chat.models.message :as models.message] [status-im.utils.handlers :as handlers] [status-im.i18n :as i18n] [status-im.utils.platform :as platform])) @@ -9,45 +10,38 @@ (defn- generate-context "Generates context for jail call" - [current-account-id chat-id to group-id] + [current-account-id chat-id group-chat? to] (merge {:platform platform/os :from current-account-id :to to :chat {:chat-id chat-id - :group-chat (not (nil? group-id))}} + :group-chat (boolean group-chat?)}} i18n/delimeters)) (defn request-command-message-data "Requests command message data from jail" - [db - {{command-name :command - content-command-name :content-command - :keys [content-command-scope-bitmask bot scope-bitmask params type]} :content - :keys [chat-id group-id jail-id] :as message} + [{:accounts/keys [current-account-id] :contacts/keys [contacts] :as db} + {{:keys [command command-scope-bitmask bot params type]} :content + :keys [chat-id group-id] :as message} {:keys [data-type] :as opts}] - (let [{:accounts/keys [current-account-id] - :contacts/keys [contacts]} db - jail-id (or bot jail-id chat-id) - jail-command-name (or content-command-name command-name)] - (if-not (get contacts jail-id) ;; bot is not even in contacts, do nothing - {:db db} - (if (get-in contacts [jail-id :jail-loaded?]) - (let [path [(if (= :response (keyword type)) :responses :commands) - [jail-command-name - (or content-command-scope-bitmask scope-bitmask)] - data-type] - to (get-in contacts [chat-id :address]) - jail-params {:parameters params - :context (generate-context current-account-id chat-id to group-id)}] - {:db db - :call-jail {:jail-id jail-id - :path path - :params jail-params - :callback-events-creator (fn [jail-response] - [[::jail-command-data-response - jail-response message opts]])}}) - {:db (update-in db [:contacts/contacts jail-id :jail-loaded-events] - conj [:request-command-message-data message opts])})))) + (if-not (get contacts bot) ;; bot is not even in contacts, do nothing + {:db db} + (if (get-in contacts [bot :jail-loaded?]) + (let [path [(if (= :response (keyword type)) :responses :commands) + [command command-scope-bitmask] + data-type] + to (get-in contacts [chat-id :address]) + jail-params {:parameters params + :context (generate-context current-account-id chat-id (models.message/group-message? message) to)}] + {:db db + :call-jail {:jail-id bot + :path path + :params jail-params + :callback-event-creator (fn [jail-response] + [::jail-command-data-response + jail-response message opts])}}) + {:db (update-in db [:contacts/contacts bot :jail-loaded-events] + conj [:request-command-message-data message opts])}))) ;;;; Handlers @@ -60,7 +54,7 @@ (handlers/register-handler-fx :request-command-message-data - [re-frame/trim-v (re-frame/inject-cofx :get-local-storage-data)] + [re-frame/trim-v (re-frame/inject-cofx :data-store/get-local-storage-data)] (fn [{:keys [db]} [message opts]] (request-command-message-data db message opts))) diff --git a/src/status_im/chat/events/console.cljs b/src/status_im/chat/events/console.cljs index 1a61f5f604..e086376f64 100644 --- a/src/status_im/chat/events/console.cljs +++ b/src/status_im/chat/events/console.cljs @@ -6,30 +6,29 @@ [taoensso.timbre :as log] [status-im.i18n :as i18n] [goog.string :as gstring] - goog.string.format)) + goog.string.format + [status-im.utils.handlers :as handlers])) ;;;; Helper fns (defn console-respond-command-messages - [command random-id-seq] - (let [{:keys [command handler-data]} command] - (when command - (let [{:keys [name]} command] - (case name - "js" (let [{:keys [err data messages]} handler-data - content (or err data) - message-events (mapv (fn [{:keys [message type]} id] - (console-chat/console-message - {:message-id id - :content (str type ": " message) - :content-type constants/text-content-type})) - messages random-id-seq)] - (conj message-events - (console-chat/console-message - {:message-id (first random-id-seq) - :content (str content) - :content-type constants/text-content-type}))) - (log/debug "ignoring command: " name)))))) + [{:keys [name] :as command} handler-data random-id-seq] + (when command + (case name + "js" (let [{:keys [err data messages]} handler-data + content (or err data) + message-events (mapv (fn [{:keys [message type]} id] + (console-chat/console-message + {:message-id id + :content (str type ": " message) + :content-type constants/text-content-type})) + messages random-id-seq)] + (conj message-events + (console-chat/console-message + {:message-id (first random-id-seq) + :content (str content) + :content-type constants/text-content-type}))) + (log/debug "ignoring command: " name)))) (defn faucet-base-url->url [url] (str url "/donate/0x%s")) @@ -42,7 +41,7 @@ (def console-commands->fx {"faucet" - (fn [{:keys [db random-id]} {:keys [params]}] + (fn [{:keys [db random-id] :as cofx} {:keys [params]}] (let [{:accounts/keys [accounts current-account-id]} db current-address (get-in accounts [current-account-id :address]) faucet-url (faucet-base-url->url (:url params))] @@ -58,19 +57,19 @@ (i18n/label :t/faucet-error)))}})) "debug" - (fn [{:keys [db random-id now]} {:keys [params]}] + (fn [{:keys [db random-id now] :as cofx} {:keys [params]}] (let [debug? (= "On" (:mode params))] - (-> {:db db} - (accounts-events/account-update {:debug? debug? - :last-updated now}) - (assoc :dispatch-n (if debug? - [[:initialize-debugging {:force-start? true}] - [:chat-received-message/add - (console-chat/console-message - {:message-id random-id - :content (i18n/label :t/debug-enabled) - :content-type constants/text-content-type})]] - [[:stop-debugging]])))))}) + (handlers/merge-fx cofx + {:dispatch-n (if debug? + [[:initialize-debugging {:force-start? true}] + [:chat-received-message/add + (console-chat/console-message + {:message-id random-id + :content (i18n/label :t/debug-enabled) + :content-type constants/text-content-type})]] + [[:stop-debugging]])} + (accounts-events/account-update {:debug? debug? + :last-updated now}))))}) (def commands-names (set (keys console-commands->fx))) diff --git a/src/status_im/chat/events/input.cljs b/src/status_im/chat/events/input.cljs index 3c001674cc..64908092f7 100644 --- a/src/status_im/chat/events/input.cljs +++ b/src/status_im/chat/events/input.cljs @@ -56,7 +56,7 @@ chat-text (if append? (str current-input new-input) new-input)] - (cond-> db + (cond-> (model/set-chat-ui-props db {:validation-messages nil}) true (assoc-in [:chats current-chat-id :input-text] (input-model/text->emoji chat-text)) @@ -146,12 +146,12 @@ {:call-jail {:jail-id owner-id :path path :params params - :callback-events-creator (fn [jail-response] - [[:chat-received-message/bot-response - {:chat-id current-chat-id - :command command - :parameter-index parameter-index} - jail-response]])}})))) + :callback-event-creator (fn [jail-response] + [:chat-received-message/bot-response + {:chat-id current-chat-id + :command command + :parameter-index parameter-index} + jail-response])}})))) (defn chat-input-focus "Returns fx for focusing on active chat input reference" @@ -179,15 +179,13 @@ (defn select-chat-input-command "Selects command + (optional) arguments as input for active chat" - [{:keys [current-chat-id chat-ui-props] :as db} - {:keys [prefill prefill-bot-db sequential-params name owner-id] :as command} metadata prevent-auto-focus?] - (let [db' (-> db + [{:keys [prefill prefill-bot-db sequential-params name owner-id] :as command} metadata prevent-auto-focus? {:keys [db]}] + (let [{:keys [current-chat-id chat-ui-props]} db + db' (-> db (bots-events/clear-bot-db owner-id) clear-seq-arguments (model/set-chat-ui-props {:show-suggestions? false - :result-box nil - :validation-messages nil - :prev-command name}) + :result-box nil}) (set-chat-input-metadata metadata) (set-chat-input-text (str (commands-model/command-name command) constants/spacing-char @@ -237,16 +235,16 @@ ;; function creating "message shaped" data from command, because that's what `request-command-message-data` expects (defn- command->message - [{:keys [bot-db current-chat-id chats]} {:keys [command] :as command-params}] - (cond-> {:chat-id current-chat-id - :jail-id (:owner-id command) - :content {:command (:name command) - :type (:type command) - :scope-bitmask (:scope-bitmask command) - :params (assoc (input-model/args->params command-params) - :bot-db (get bot-db (:owner-id command)))}} - (get-in chats [current-chat-id :group-chat]) - (assoc :group-id current-chat-id))) + [{:keys [bot-db current-chat-id chats]} {:keys [command] :as command-params}] + (message-model/add-message-type + {:chat-id current-chat-id + :content {:bot (:owner-id command) + :command (:name command) + :type (:type command) + :command-scope-bitmask (:scope-bitmask command) + :params (assoc (input-model/args->params command-params) + :bot-db (get bot-db (:owner-id command)))}} + (get chats current-chat-id))) (defn proceed-command "Proceed with command processing by creating command message + setting up and executing chain of events: @@ -260,8 +258,7 @@ :to-message (:to-message-id metadata) :created-at current-time :id message-id - :chat-id current-chat-id - :jail-id (:jail-id message)} + :chat-id current-chat-id} event-chain {:data-type :validator :proceed-event-creator (fn [validation-response] [::proceed-validation @@ -282,35 +279,21 @@ ;;;; Handlers -(handlers/register-handler-db - :update-input-data - (fn [db] - (input-model/modified-db-after-change db))) - (handlers/register-handler-fx :set-chat-input-text [re-frame/trim-v] (fn [{:keys [db]} [text]] - (-> (set-chat-input-text db text) - (call-on-message-input-change)))) - -(handlers/register-handler-db - :add-to-chat-input-text - [re-frame/trim-v] - (fn [db [text-to-add]] - (set-chat-input-text db text-to-add :append? true))) + (let [new-db (set-chat-input-text db text) + fx (call-on-message-input-change new-db)] + (if-let [{:keys [command]} (input-model/selected-chat-command new-db)] + (merge fx (load-chat-parameter-box new-db command)) + fx)))) (handlers/register-handler-fx :select-chat-input-command [re-frame/trim-v] - (fn [{:keys [db]} [command metadata prevent-auto-focus?]] - (select-chat-input-command db command metadata prevent-auto-focus?))) - -(handlers/register-handler-db - :set-chat-input-metadata - [re-frame/trim-v] - (fn [db [data]] - (set-chat-input-metadata db data))) + (fn [cofx [command metadata prevent-auto-focus?]] + (select-chat-input-command command metadata prevent-auto-focus? cofx))) (handlers/register-handler-db :set-command-argument @@ -331,39 +314,18 @@ (when-let [cmp-ref (get-in chat-ui-props [current-chat-id ref])] {::blur-rn-component cmp-ref}))) -(handlers/register-handler-fx - :load-chat-parameter-box - [re-frame/trim-v] - (fn [{:keys [db]} [command]] - (load-chat-parameter-box db command))) - (handlers/register-handler-fx ::proceed-validation [re-frame/trim-v] - (fn [_ [{:keys [markup validationHandler parameters]} proceed-events]] + (fn [_ [{:keys [markup parameters]} proceed-events]] (let [error-events-creator (fn [validator-result] [[:set-chat-ui-props {:validation-messages validator-result :sending-in-progress? false}]]) - events (cond - markup + events (if markup (error-events-creator markup) - - validationHandler - [[::execute-validation-handler - validationHandler parameters error-events-creator proceed-events]] - - :default proceed-events)] {:dispatch-n events}))) -(handlers/register-handler-fx - ::execute-validation-handler - [re-frame/trim-v] - (fn [_ [validation-handler-name params error-events-creator proceed-events]] - (let [error-events (when-let [validator (input-model/validation-handler validation-handler-name)] - (validator params error-events-creator))] - {:dispatch-n (or error-events proceed-events)}))) - (handlers/register-handler-fx ::send-command message-model/send-interceptors @@ -432,12 +394,6 @@ (command-not-complete-fx db input-text)) (plain-text-message-fx db cofx input-text current-chat-id current-public-key)))))) -;; TODO: remove this handler and leave only helper fn once all invocations are refactored -(handlers/register-handler-db - :clear-seq-arguments - (fn [db] - (clear-seq-arguments db))) - (handlers/register-handler-db ::update-seq-arguments [re-frame/trim-v] diff --git a/src/status_im/chat/events/receive_message.cljs b/src/status_im/chat/events/receive_message.cljs index 093c2c4316..74915674af 100644 --- a/src/status_im/chat/events/receive_message.cljs +++ b/src/status_im/chat/events/receive_message.cljs @@ -1,7 +1,6 @@ (ns status-im.chat.events.receive-message (:require [re-frame.core :as re-frame] - [taoensso.timbre :as log] - [status-im.data-store.messages :as messages-store] + [taoensso.timbre :as log] [status-im.chat.events.commands :as commands-events] [status-im.chat.models.message :as message-model] [status-im.constants :as constants] @@ -14,7 +13,7 @@ ::received-message message-model/receive-interceptors (fn [cofx [message]] - (message-model/receive cofx message))) + (message-model/receive message cofx))) (handlers/register-handler-fx :chat-received-message/add @@ -39,7 +38,7 @@ {:short-preview short-preview :preview preview})])}])}) ;; regular non command message, we can add it right away - (message-model/receive cofx message))))) + (message-model/receive message cofx))))) ;; TODO(alwx): refactor this when status-im.commands.handlers.jail is refactored (handlers/register-handler-fx diff --git a/src/status_im/chat/events/requests.cljs b/src/status_im/chat/events/requests.cljs index ef404f40a5..a69c6d20c7 100644 --- a/src/status_im/chat/events/requests.cljs +++ b/src/status_im/chat/events/requests.cljs @@ -1,43 +1,26 @@ (ns status-im.chat.events.requests (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :as handlers] - [status-im.data-store.requests :as requests-store])) - -;; Coeffects - -(re-frame/reg-cofx - :get-stored-unanswered-requests - (fn [cofx _] - (assoc cofx :stored-unanswered-requests (requests-store/get-all-unanswered)))) - -;; Effects -(re-frame/reg-fx - ::mark-as-answered - (fn [{:keys [chat-id message-id]}] - (requests-store/mark-as-answered chat-id message-id))) - -(re-frame/reg-fx - ::save-request - (fn [request] - (requests-store/save request))) + [status-im.constants :as constants] + [status-im.utils.handlers :as handlers])) ;; Functions (defn request-answered - "Takes fx, chat-id and message, updates fx with necessary data for markin request as answered" - [fx chat-id message-id] - (-> fx - (update-in [:db :chats chat-id :requests] dissoc message-id) - (assoc ::mark-as-answered {:chat-id chat-id - :message-id message-id}))) + "Takes chat-id, message-id and cofx, returns fx necessary data for marking request as answered" + [chat-id message-id {:keys [db]}] + (when message-id + {:db (update-in db [:chats chat-id :requests] dissoc message-id) + :data-store/mark-request-as-answered {:chat-id chat-id + :message-id message-id}})) (defn add-request - "Takes fx, chat-id and message, updates fx with necessary data for adding new request" - [fx chat-id {:keys [message-id content]}] - (let [request {:chat-id chat-id - :message-id message-id - :response (:command content) - :status "open"}] - (-> fx - (assoc-in [:db :chats chat-id :requests message-id] request) - (assoc ::save-request request)))) + "Takes chat-id, message-id + cofx and returns fx with necessary data for adding new request" + [chat-id message-id {:keys [db]}] + (let [{:keys [content-type content]} (get-in db [:chats chat-id :messages message-id])] + (when (= content-type constants/content-type-command-request) + (let [request {:chat-id chat-id + :message-id message-id + :response (:request-command content) + :status "open"}] + {:db (assoc-in db [:chats chat-id :requests message-id] request) + :data-store/save-request request})))) diff --git a/src/status_im/chat/events/send_message.cljs b/src/status_im/chat/events/send_message.cljs index e172494ab2..be007eeb6f 100644 --- a/src/status_im/chat/events/send_message.cljs +++ b/src/status_im/chat/events/send_message.cljs @@ -3,8 +3,7 @@ [re-frame.core :as re-frame] [status-im.chat.models.message :as message-model] [status-im.native-module.core :as status] - [status-im.protocol.core :as protocol] - [status-im.utils.handlers :as handlers] + [status-im.utils.handlers :as handlers] [status-im.utils.types :as types])) (re-frame/reg-fx @@ -15,21 +14,6 @@ (log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json) (status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %))))) -(re-frame/reg-fx - :send-group-message - (fn [value] - (protocol/send-group-message! value))) - -(re-frame/reg-fx - :send-public-group-message - (fn [value] - (protocol/send-public-group-message! value))) - -(re-frame/reg-fx - :send-message - (fn [value] - (protocol/send-message! value))) - ;;;; Handlers (handlers/register-handler-fx diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs deleted file mode 100644 index e1b75c6ce0..0000000000 --- a/src/status_im/chat/handlers.cljs +++ /dev/null @@ -1,195 +0,0 @@ -(ns status-im.chat.handlers - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.chat.models :as models] - [status-im.i18n :as i18n] - [status-im.protocol.core :as protocol] - [status-im.ui.components.styles :as components.styles] - [status-im.utils.handlers :as handlers] - [status-im.utils.random :as random] - status-im.chat.events - [status-im.utils.datetime :as datetime])) - -(handlers/register-handler - :leave-group-chat - ;; todo order of operations tbd - (re-frame/after (fn [_ _] (re-frame/dispatch [:navigation-replace :home]))) - (handlers/side-effect! - (fn [{:keys [web3 current-chat-id chats current-public-key]} _] - (let [{:keys [public-key private-key public?]} (chats current-chat-id)] - (protocol/stop-watching-group! - {:web3 web3 - :group-id current-chat-id}) - (when-not public? - (protocol/leave-group-chat! - {:web3 web3 - :group-id current-chat-id - :keypair {:public public-key - :private private-key} - :message {:from current-public-key - :message-id (random/id)}}))) - (re-frame/dispatch [:remove-chat current-chat-id])))) - -(handlers/register-handler-fx - :leave-group-chat? - (fn [] - {:show-confirmation {:title (i18n/label :t/leave-confirmation) - :content (i18n/label :t/leave-group-chat-confirmation) - :confirm-button-text (i18n/label :t/leave) - :on-accept #(re-frame/dispatch [:leave-group-chat])}})) - -(handlers/register-handler :update-group-message - (handlers/side-effect! - (fn [{:keys [current-public-key web3 chats]} - [_ {:keys [from] - {:keys [group-id keypair timestamp]} :payload}]] - (let [{:keys [private public]} keypair - {:keys [group-admin is-active] :as chat} (get chats group-id)] - (when (and (= from group-admin - (or (nil? chat) - (models/new-update? chat timestamp)))) - (re-frame/dispatch [:update-chat! {:chat-id group-id - :public-key public - :private-key private - :updated-at timestamp}]) - (when is-active - (protocol/start-watching-group! - {:web3 web3 - :group-id group-id - :identity current-public-key - :keypair keypair - :callback #(re-frame/dispatch [:incoming-message %1 %2])}))))))) - -(re-frame/reg-fx - ::start-watching-group - (fn [{:keys [group-id web3 current-public-key keypair]}] - (protocol/start-watching-group! - {:web3 web3 - :group-id group-id - :identity current-public-key - :keypair keypair - :callback #(re-frame/dispatch [:incoming-message %1 %2])}))) - -(handlers/register-handler-fx - :create-new-public-chat - [(re-frame/inject-cofx :now)] - (fn [{:keys [db now]} [_ topic]] - (let [exists? (boolean (get-in db [:chats topic])) - chat {:chat-id topic - :name topic - :color components.styles/default-chat-color - :group-chat true - :public? true - :is-active true - :timestamp now}] - (merge - (when-not exists? - {:db (assoc-in db [:chats (:chat-id chat)] chat) - :save-chat chat - ::start-watching-group (merge {:group-id topic} - (select-keys db [:web3 :current-public-key]))}) - {:dispatch-n [[:navigate-to-clean :home] - [:navigate-to-chat topic]]})))) - -(re-frame/reg-fx - ::start-listen-group - (fn [{:keys [new-chat web3 current-public-key]}] - (let [{:keys [chat-id public-key private-key contacts name]} new-chat - identities (mapv :identity contacts)] - (protocol/invite-to-group! - {:web3 web3 - :group {:id chat-id - :name name - :contacts (conj identities current-public-key) - :admin current-public-key - :keypair {:public public-key - :private private-key}} - :identities identities - :message {:from current-public-key - :message-id (random/id)}}) - (protocol/start-watching-group! - {:web3 web3 - :group-id chat-id - :identity current-public-key - :keypair {:public public-key - :private private-key} - :callback #(re-frame/dispatch [:incoming-message %1 %2])})))) - -(defn group-name-from-contacts [contacts selected-contacts username] - (->> (select-keys contacts selected-contacts) - vals - (map :name) - (cons username) - (string/join ", "))) - -(defn prepare-group-chat - [{:keys [current-public-key username] - :group/keys [selected-contacts] - :contacts/keys [contacts]} group-name timestamp] - (let [selected-contacts' (mapv #(hash-map :identity %) selected-contacts) - chat-name (if-not (string/blank? group-name) - group-name - (group-name-from-contacts contacts selected-contacts username)) - {:keys [public private]} (protocol/new-keypair!)] - {:chat-id (random/id) - :public-key public - :private-key private - :name chat-name - :color components.styles/default-chat-color - :group-chat true - :group-admin current-public-key - :is-active true - :timestamp timestamp - :contacts selected-contacts'})) - -(handlers/register-handler-fx - :create-new-group-chat-and-open - [(re-frame/inject-cofx :now)] - (fn [{:keys [db now]} [_ group-name]] - (let [new-chat (prepare-group-chat (select-keys db [:group/selected-contacts :current-public-key :username - :contacts/contacts]) - group-name - now)] - {:db (-> db - (assoc-in [:chats (:chat-id new-chat)] new-chat) - (assoc :group/selected-contacts #{})) - :save-chat new-chat - ::start-listen-group (merge {:new-chat new-chat} - (select-keys db [:web3 :current-public-key])) - :dispatch-n [[:navigate-to-clean :home] - [:navigate-to-chat (:chat-id new-chat)]]}))) - -(handlers/register-handler-fx - :group-chat-invite-received - (fn [{{:keys [current-public-key] :as db} :db} - [_ {:keys [from] - {:keys [group-id group-name contacts keypair timestamp]} :payload}]] - (let [{:keys [private public]} keypair] - (let [contacts' (keep (fn [ident] - (when (not= ident current-public-key) - {:identity ident})) contacts) - chat (get-in db [:chats group-id]) - new-chat {:chat-id group-id - :name group-name - :group-chat true - :group-admin from - :public-key public - :private-key private - :contacts contacts' - :added-to-at timestamp - :timestamp timestamp - :is-active true}] - (when (or (nil? chat) - (models/new-update? chat timestamp)) - {::start-watching-group (merge {:group-id group-id - :keypair keypair} - (select-keys db [:web3 :current-public-key])) - :dispatch (if chat - [:update-chat! new-chat] - [:add-chat group-id new-chat])}))))) - -(handlers/register-handler-fx - :show-profile - (fn [{db :db} [_ identity]] - {:db (assoc db :contacts/identity identity) - :dispatch [:navigate-forget :profile]})) diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index f9dd7d0911..4243fbed49 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -1,6 +1,8 @@ (ns status-im.chat.models (:require [status-im.ui.components.styles :as styles] - [status-im.utils.gfycat.core :as gfycat])) + [status-im.utils.gfycat.core :as gfycat] + [status-im.utils.handlers :as handlers] + [status-im.transport.core :as transport])) (defn set-chat-ui-props "Updates ui-props in active chat by merging provided kvs into them" @@ -13,67 +15,105 @@ (update-in db [:chat-ui-props current-chat-id ui-element] not)) (defn- create-new-chat - [{:keys [db now]} chat-id] + [chat-id {:keys [db now]}] (let [name (get-in db [:contacts/contacts chat-id :name])] - {:chat-id chat-id - :name (or name (gfycat/generate-gfy chat-id)) - :color styles/default-chat-color - :group-chat false - :is-active true - :timestamp now - :contacts [{:identity chat-id}]})) + {:chat-id chat-id + :name (or name (gfycat/generate-gfy chat-id)) + :color styles/default-chat-color + :group-chat false + :is-active true + :timestamp now + :contacts [{:identity chat-id}] + :last-from-clock-value 0 + :last-to-clock-value 0})) (defn add-chat "Adds new chat to db & realm, if the chat with same id already exists, justs restores it" - ([cofx chat-id] - (add-chat cofx chat-id {})) - ([{:keys [db get-stored-chat] :as cofx} chat-id chat-props] + ([chat-id cofx] + (add-chat chat-id {} cofx)) + ([chat-id chat-props {:keys [db get-stored-chat] :as cofx}] (let [{:keys [deleted-chats]} db new-chat (merge (if (get deleted-chats chat-id) (assoc (get-stored-chat chat-id) :is-active true) - (create-new-chat cofx chat-id)) + (create-new-chat chat-id cofx)) chat-props)] {:db (-> db (update :chats assoc chat-id new-chat) (update :deleted-chats (fnil disj #{}) chat-id)) - :save-chat new-chat}))) + :data-store/save-chat new-chat}))) + +(defn add-public-chat + "Adds new public group chat to db & realm" + [topic {:keys [db now] :as cofx}] + (let [chat {:chat-id topic + :name topic + :color styles/default-chat-color + :group-chat true + :public? true + :is-active true + :timestamp now + :last-to-clock-value 0 + :last-from-clock-value 0}] + {:db (assoc-in db [:chats topic] chat) + :data-store/save-chat chat})) + +(defn add-group-chat + "Adds new private group chat to db & realm" + [chat-id chat-name admin participants {:keys [db now] :as cofx}] + (let [chat {:chat-id chat-id + :name chat-name + :color styles/default-chat-color + :group-chat true + :group-admin admin + :is-active true + :timestamp now + :contacts (mapv (partial hash-map :identity) participants) + :last-to-clock-value 0 + :last-from-clock-value 0}] + {:db (assoc-in db [:chats chat-id] chat) + :data-store/save-chat chat})) ;; TODO (yenda): there should be an option to update the timestamp ;; this shouldn't need a specific function like `upsert-chat` which ;; is wrongfuly named (defn update-chat "Updates chat properties when not deleted, if chat is not present in app-db, creates a default new one" - [{:keys [db] :as cofx} {:keys [chat-id] :as chat-props}] + [{:keys [chat-id] :as chat-props} {:keys [db] :as cofx}] (let [{:keys [chats deleted-chats]} db] (if (get deleted-chats chat-id) ;; when chat is deleted, don't change anything {:db db} (let [chat (merge (or (get chats chat-id) - (create-new-chat cofx chat-id)) + (create-new-chat chat-id cofx)) chat-props)] {:db (update-in db [:chats chat-id] merge chat) - :save-chat chat})))) + :data-store/save-chat chat})))) ;; TODO (yenda): an upsert is suppose to add the entry if it doesn't ;; exist and update it if it does (defn upsert-chat "Just like `update-chat` only implicitely updates timestamp" - [cofx chat] - (update-chat cofx (assoc chat :timestamp (:now cofx)))) + [chat cofx] + (update-chat (assoc chat :timestamp (:now cofx)) cofx)) (defn new-update? [{:keys [added-to-at removed-at removed-from-at]} timestamp] (and (> timestamp added-to-at) (> timestamp removed-at) (> timestamp removed-from-at))) -(defn remove-chat [{:keys [db]} chat-id] - (let [{:keys [chat-id group-chat debug?]} (get-in db [:chats chat-id])] - (cond-> {:db (-> db - (update :chats dissoc chat-id) - (update :deleted-chats (fnil conj #{}) chat-id)) - :delete-pending-messages chat-id} - (or group-chat debug?) - (assoc :delete-messages chat-id) - debug? - (assoc :delete-chat chat-id) - (not debug?) - (assoc :deactivate-chat chat-id)))) +(defn remove-chat [chat-id {:keys [db] :as cofx}] + (let [{:keys [chat-id group-chat debug?]} (get-in db [:chats chat-id]) + fx (cond-> {:db (-> db + (update :chats dissoc chat-id) + (update :deleted-chats (fnil conj #{}) chat-id))} + (or group-chat debug?) + (assoc :data-store/delete-messages chat-id) + debug? + (assoc :data-store/delete-chat chat-id) + (not debug?) + (assoc :data-store/deactivate-chat chat-id))] + (handlers/merge-fx cofx fx (transport/unsubscribe-from-chat chat-id)))) + +(defn bot-only-chat? [db chat-id] + (let [{:keys [group-chat contacts]} (get-in db [:chats chat-id])] + (and (not group-chat) + (get-in db [:contacts/contacts (:identity (first contacts)) :dapp?])))) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index 85bdbbeb04..f6d27c6172 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -1,12 +1,8 @@ (ns status-im.chat.models.input (:require [clojure.string :as str] [goog.object :as object] - [status-im.ui.components.react :as rc] - [status-im.native-module.core :as status] [status-im.chat.constants :as const] [status-im.chat.models.commands :as commands-model] - [status-im.chat.views.input.validation-messages :refer [validation-message]] - [status-im.i18n :as i18n] [status-im.js-dependencies :as dependencies] [taoensso.timbre :as log])) @@ -203,28 +199,3 @@ [(keyword (get-in params [i :name])) value])) (remove #(nil? (first %))) (into {})))) - -(defn modified-db-after-change - "Returns the new db object that should be used after any input change." - [{:keys [current-chat-id] :as db}] - (let [command (selected-chat-command db) - prev-command (get-in db [:chat-ui-props current-chat-id :prev-command])] - (if command - (cond-> db - ;; clear the bot db - (not= prev-command (-> command :command :name)) - (assoc-in [:bot-db (or (:bot command) current-chat-id)] nil) - ;; clear the chat's validation messages - true - (assoc-in [:chat-ui-props current-chat-id :validation-messages] nil)) - (-> db - ;; clear input metadata - (assoc-in [:chats current-chat-id :input-metadata] nil) - ;; clear - (update-in [:chat-ui-props current-chat-id] - merge - {:result-box nil - :validation-messages nil - :prev-command (-> command :command :name)}))))) - -(defmulti validation-handler (fn [name] (keyword name))) diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index bc1947f527..8000630545 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -6,14 +6,14 @@ [status-im.chat.events.requests :as requests-events] [status-im.chat.models :as chat-model] [status-im.chat.models.commands :as commands-model] - [status-im.utils.clocks :as clocks-utils])) - -(defn- get-current-account - [{:accounts/keys [accounts current-account-id]}] - (get accounts current-account-id)) + [status-im.utils.clocks :as clocks-utils] + [status-im.utils.handlers :as handlers] + [status-im.transport.utils :as transport.utils] + [status-im.transport.message.core :as transport] + [status-im.transport.message.v1.protocol :as protocol])) (def receive-interceptors - [(re-frame/inject-cofx :get-stored-message) (re-frame/inject-cofx :get-stored-chat) + [(re-frame/inject-cofx :data-store/get-message) (re-frame/inject-cofx :data-store/get-chat) (re-frame/inject-cofx :random-id) re-frame/trim-v]) (defn- lookup-response-ref @@ -25,71 +25,133 @@ contacts)] (:ref (get available-commands-responses response-name)))) -(defn add-message-to-db - [db chat-id {:keys [message-id clock-value] :as message} current-chat?] - (let [prepared-message (cond-> (assoc message - :chat-id chat-id - :appearing? true) +(defn- add-message + [chat-id {:keys [message-id from-clock-value to-clock-value] :as message} current-chat? {:keys [db]}] + (let [prepared-message (cond-> (assoc message :appearing? true) (not current-chat?) (assoc :appearing? false))] - (cond-> (-> db - (update-in [:chats chat-id :messages] assoc message-id prepared-message) - (update-in [:chats chat-id :last-clock-value] (fnil max 0) clock-value)) - (not current-chat?) - (update-in [:chats chat-id :unviewed-messages] (fnil conj #{}) message-id)))) + {:db (cond-> (-> db + (update-in [:chats chat-id :messages] dissoc from-clock-value) + (update-in [:chats chat-id :messages] assoc message-id prepared-message) + (update-in [:chats chat-id :last-from-clock-value] max from-clock-value) + (update-in [:chats chat-id :last-to-clock-value] max to-clock-value)) + (not current-chat?) + (update-in [:chats chat-id :unviewed-messages] (fnil conj #{}) message-id)) + :data-store/save-message prepared-message})) + +;; We start with [0 0] ([from-clock-value to-clock-value]) for each participant of 1-1 chat (local perspective on each device). +;; Now for sending, we always increment the to-clock-value and include it in message payload being sent (so only to-clock-value is present in network message). +;; Locally, the sent message always replicates the latest-from-clock-value of the chat. +;; Upon receiving message, receiver reads the to-clock-value of the received message and sets that to be the from-clock-value locally +;; (this will be also the new latest-from-clock-value of the chat), to-clock-value for the message is the latest-to-clock-value of the 1-1 chat`. + +;; All this ensures, that there will be no [from-clock-value to-clock-value] duplicate in chat message on each device + the local order will appear consistent, +;; even if it’s possible it won’t be the same on both devices (for example user A sends 5 messages, during the sending, +;; he receives the message from user B, so his local order will be A1, A2, B, A3, A4, A5, but his messages will take a long time to reach user B, +;; for some reason, so user B will see it as B, A1, A2, A3, A4, A5). +;; I don’t think that’s very problematic and I don’t think we can do much about it without single source of truth where order received messages are serialised +;; and definite order is established (server), it is the case even in the current implementation. +(defn- prepare-chat [chat-id {:keys [db] :as cofx}] + (if (get-in db [:chats chat-id]) + (chat-model/upsert-chat {:chat-id chat-id} cofx) + (chat-model/add-chat chat-id cofx))) + +(defn- get-current-account [{:accounts/keys [accounts current-account-id]}] + (get accounts current-account-id)) + +(defn- send-message-seen [chat-id message-id send-seen? cofx] + (when send-seen? + (transport/send (protocol/map->MessagesSeen {:message-ids #{message-id}}) chat-id cofx))) + +(defn- placeholder-message [chat-id from timestamp temp-id to-clock] + {:message-id temp-id + :outgoing false + :chat-id chat-id + :from from + :to "me" + :content "Waiting for message to arrive..." + :content-type constants/content-type-placeholder + :show? true + :from-clock-value temp-id + :to-clock-value to-clock + :timestamp timestamp}) + +(defn- add-placeholder-messages [chat-id from timestamp old-from-clock to-clock new-from-clock {:keys [db]}] + (when (> (- new-from-clock old-from-clock) 1) + {:db (reduce (fn [db temp-id] + (assoc-in db [:chats chat-id :messages temp-id] (placeholder-message chat-id from timestamp temp-id to-clock))) + db + (range (inc old-from-clock) new-from-clock))})) + +(defn- add-received-message + [{:keys [from message-id chat-id content content-type timestamp to-clock-value] :as message} + {:keys [db now] :as cofx}] + (let [{:keys [current-chat-id + view-id + access-scope->commands-responses] + :contacts/keys [contacts]} db + {:keys [public-key] :as current-account} (get-current-account db) + current-chat? (and (= :chat view-id) (= current-chat-id chat-id)) + {:keys [last-from-clock-value + last-to-clock-value] :as chat} (get-in db [:chats chat-id]) + request-command (:request-command content) + command-request? (and (= content-type constants/content-type-command-request) + request-command) + new-from-clock-value (or to-clock-value (inc last-from-clock-value)) + new-timestamp (or timestamp now)] + (handlers/merge-fx cofx + (add-message chat-id + (cond-> (assoc message + :timestamp new-timestamp + :show? true + :from-clock-value new-from-clock-value + :to-clock-value last-to-clock-value) + public-key + (assoc :user-statuses {public-key (if current-chat? :seen :received)}) + command-request? + (assoc-in [:content :request-command-ref] + (lookup-response-ref access-scope->commands-responses + current-account chat contacts request-command))) + current-chat?) + (send-message-seen chat-id message-id (and public-key + current-chat? + (not (chat-model/bot-only-chat? db chat-id)) + (not (= constants/system from)))) + (add-placeholder-messages chat-id from new-timestamp last-from-clock-value last-to-clock-value new-from-clock-value)))) (defn receive - [{:keys [db now] :as cofx} - {:keys [from group-id chat-id content-type content message-id timestamp clock-value] - :as message}] - (let [{:keys [current-chat-id view-id - access-scope->commands-responses] :contacts/keys [contacts]} db - {:keys [public-key] :as current-account} (get-current-account db) - chat-identifier (or group-id chat-id from) - current-chat? (and (= :chat view-id) - (= current-chat-id chat-identifier)) - fx (if (get-in db [:chats chat-identifier]) - (chat-model/upsert-chat cofx {:chat-id chat-identifier - :group-chat (boolean group-id)}) - (chat-model/add-chat cofx chat-identifier)) - chat (get-in fx [:db :chats chat-identifier]) - command-request? (= content-type constants/content-type-command-request) - command (:command content) - enriched-message (cond-> (assoc message - :chat-id chat-identifier - :timestamp (or timestamp now) - :show? true - :clock-value (clocks-utils/receive - clock-value - (:last-clock-value chat))) - public-key - (assoc :user-statuses {public-key (if current-chat? :seen :received)}) - (and command command-request?) - (assoc-in [:content :content-command-ref] - (lookup-response-ref access-scope->commands-responses - current-account - (get-in fx [:db :chats chat-identifier]) - contacts - command)))] - (cond-> (-> fx - (update :db add-message-to-db chat-identifier enriched-message current-chat?) - (assoc :save-message (dissoc enriched-message :new?))) - command-request? - (requests-events/add-request chat-identifier enriched-message)))) + [{:keys [chat-id message-id] :as message} cofx] + (handlers/merge-fx cofx + (prepare-chat chat-id) + (add-received-message message) + (requests-events/add-request chat-id message-id))) + +(defn system-message [chat-id message-id timestamp content] + {:message-id message-id + :outgoing false + :chat-id chat-id + :from constants/system + :username constants/system + :timestamp timestamp + :show? true + :content content + :content-type constants/text-content-type}) + +(defn group-message? [{:keys [message-type]}] + (#{:group-user-message :public-group-user-message} message-type)) (defn add-to-chat? - [{:keys [db get-stored-message]} {:keys [group-id chat-id from message-id]}] - (let [chat-identifier (or group-id chat-id from) - {:keys [chats deleted-chats current-public-key]} db - {:keys [messages not-loaded-message-ids]} (get chats chat-identifier)] + [{:keys [db get-stored-message]} {:keys [chat-id from message-id] :as message}] + (let [{:keys [chats deleted-chats current-public-key]} db + {:keys [messages not-loaded-message-ids]} (get chats chat-id)] (when (not= from current-public-key) - (if group-id - (not (or (get deleted-chats chat-identifier) + (if (group-message? message) + (not (or (get deleted-chats chat-id) (get messages message-id) (get not-loaded-message-ids message-id))) (not (or (get messages message-id) (get not-loaded-message-ids message-id) - (and (get deleted-chats chat-identifier) + (and (get deleted-chats chat-id) (get-stored-message message-id)))))))) (defn message-seen-by? [message user-pk] @@ -99,7 +161,7 @@ (def send-interceptors [(re-frame/inject-cofx :random-id) (re-frame/inject-cofx :random-id-seq) - (re-frame/inject-cofx :get-stored-chat) re-frame/trim-v]) + (re-frame/inject-cofx :data-store/get-chat) re-frame/trim-v]) (defn- handle-message-from-bot [{:keys [random-id] :as cofx} {:keys [message chat-id]}] (when-let [message (cond @@ -120,15 +182,12 @@ :chat-id chat-id :from chat-id :to "me"})] - (receive cofx message))) + (receive message cofx))) (defn- send-dapp-message! - [{{:accounts/keys [current-account-id] :as db} :db :as cofx} - {{:keys [message-type] - :as message} :message - :keys [chat-id command] :as args}] - (if command - (when-let [text-message (get-in command [:content :handler-data :text-message])] + [{{:accounts/keys [current-account-id] :as db} :db :as cofx} chat-id {:keys [content-type] :as message}] + (if (= content-type constants/content-type-command) + (when-let [text-message (get-in message [:content :handler-data :text-message])] (handle-message-from-bot cofx {:message text-message :chat-id chat-id})) (let [data (get-in db [:local-storage chat-id])] @@ -138,146 +197,107 @@ :context {:data data :from current-account-id}}}))) -(defn- generate-message - [{:keys [network-status]} chat-id message] - (assoc (select-keys message [:from :message-id]) - :payload (cond-> (select-keys message [:content :content-type :clock-value :timestamp :show?]) - (= :offline network-status) - (assoc :show? false)))) - -(defn send - [{{:keys [web3 chats] - :accounts/keys [accounts current-account-id] - :contacts/keys [contacts] :as db} :db :as cofx} - {:keys [chat-id command message] :as args}] +(defn- send + [chat-id send-record {{:contacts/keys [contacts]} :db :as cofx}] (let [{:keys [dapp? fcm-token]} (get contacts chat-id)] (if dapp? - (send-dapp-message! cofx args) - (let [{:keys [group-chat public?]} (get-in db [:chats chat-id]) - options {:web3 web3 - :message (generate-message db chat-id (or command message))}] - (cond - (and group-chat (not public?)) - (let [{:keys [public-key private-key]} (get chats chat-id)] - {:send-group-message (assoc options - :group-id chat-id - :keypair {:public public-key - :private private-key})}) + (send-dapp-message! cofx chat-id send-record) + (if fcm-token + (handlers/merge-fx cofx + {:send-notification {:message "message" + :payload {:title "Status" :body "You have a new message"} + :tokens [fcm-token]}} + (transport/send send-record chat-id)) + (transport/send send-record chat-id cofx))))) - (and group-chat public?) - {:send-public-group-message (assoc options :group-id chat-id - :username (get-in accounts [current-account-id :name]))} +(defn add-message-type [message {:keys [chat-id group-chat public?]}] + (cond-> message + (not group-chat) + (assoc :message-type :user-message) + (and group-chat public?) + (assoc :message-type :public-group-user-message) + (and group-chat (not public?)) + (assoc :message-type :group-user-message))) - :else - (merge {:send-message (assoc-in options [:message :to] chat-id)} - (when fcm-token {:send-notification {:message "message" - :payload {:title "Status" :body "You have a new message"} - :tokens [fcm-token]}}))))))) +(defn- prepare-plain-message [{:keys [identity message-text]} + {:keys [chat-id last-to-clock-value last-from-clock-value] :as chat} now] + (add-message-type {:chat-id chat-id + :content message-text + :from identity + :content-type constants/text-content-type + :outgoing true + :timestamp now + :to-clock-value (inc last-to-clock-value) + :from-clock-value last-from-clock-value + :show? true} + chat)) -(defn- prepare-message [params chat now random-id] - (let [{:keys [chat-id identity message-text]} params - {:keys [group-chat public? last-clock-value]} chat - message {:message-id random-id - :chat-id chat-id - :content message-text - :from identity - :content-type constants/text-content-type - :outgoing true - :timestamp now - :clock-value (clocks-utils/send last-clock-value) - :show? true}] - (cond-> message - (not group-chat) - (assoc :message-type :user-message - :to chat-id) - group-chat - (assoc :group-id chat-id) - (and group-chat public?) - (assoc :message-type :public-group-user-message) - (and group-chat (not public?)) - (assoc :message-type :group-user-message) - (not group-chat) - (assoc :to chat-id :message-type :user-message)))) +(def ^:private transport-keys [:content :content-type :message-type :to-clock-value :timestamp]) + +(defn- upsert-and-send [{:keys [chat-id] :as message} cofx] + (let [send-record (protocol/map->Message (select-keys message transport-keys)) + message-with-id (assoc message :message-id (transport.utils/message-id send-record))] + (handlers/merge-fx cofx + (chat-model/upsert-chat {:chat-id chat-id}) + (add-message chat-id message-with-id true) + (send chat-id send-record)))) (defn send-message [{:keys [db now random-id] :as cofx} {:keys [chat-id] :as params}] - (let [chat (get-in db [:chats chat-id]) - message (prepare-message params chat now random-id) - params' (assoc params :message message) - fx (-> (chat-model/upsert-chat cofx {:chat-id chat-id}) - (update :db add-message-to-db chat-id message true) - (assoc :save-message message))] - (merge fx (send cofx params')))) + (upsert-and-send (prepare-plain-message params (get-in db [:chats chat-id]) now) cofx)) -(defn- prepare-command - [identity chat-id now clock-value +(defn- prepare-command-message + [identity + {:keys [last-to-clock-value last-from-clock-value chat-id] :as chat} + now {request-params :params request-command :command :keys [prefill prefillBotDb] :as request} - {:keys [id params command to-message handler-data content-type]}] + {:keys [params command handler-data content-type]}] (let [content (if request - {:command request-command - :params (assoc request-params :bot-db (:bot-db params)) - :prefill prefill - :prefill-bot-db prefillBotDb} - {:command (:name command) - :scope (:scope command) - :params params}) - content' (assoc content :handler-data handler-data - :type (name (:type command)) - :content-command (:name command) - :content-command-scope-bitmask (:scope-bitmask command) - :content-command-ref (:ref command) - :preview (:preview command) - :short-preview (:short-preview command) - :bot (or (:bot command) - (:owner-id command)))] - {:message-id id - :from identity - :to chat-id - :timestamp now - :content content' - :content-type (or content-type - (if request - constants/content-type-command-request - constants/content-type-command)) - :outgoing true - :to-message to-message - :type (:type command) - :has-handler (:has-handler command) - :clock-value (clocks-utils/send clock-value) - :show? true})) + {:request-command request-command + ;; TODO janherich this is technically not correct, but works for now + :request-command-ref (:ref command) + :params (assoc request-params :bot-db (:bot-db params)) + :prefill prefill + :prefill-bot-db prefillBotDb} + {:params params}) + content' (assoc content + :command (:name command) + :handler-data handler-data + :type (name (:type command)) + :command-scope-bitmask (:scope-bitmask command) + :command-ref (:ref command) + :preview (:preview command) + :short-preview (:short-preview command) + :bot (:owner-id command))] + (add-message-type {:chat-id chat-id + :from identity + :timestamp now + :content content' + :content-type (or content-type + (if request + constants/content-type-command-request + constants/content-type-command)) + :outgoing true + :to-clock-value (inc last-to-clock-value) + :from-clock-value last-from-clock-value + :show? true} + chat))) + +(defn- add-console-responses + [command handler-data {:keys [random-id-seq]}] + {:dispatch-n (->> (console-events/console-respond-command-messages command handler-data random-id-seq) + (mapv (partial vector :chat-received-message/add)))}) (defn send-command - [{{:keys [current-public-key chats]} :db :keys [now random-id-seq] :as cofx} params] - (let [{{:keys [handler-data - command] - :as content} :command - chat-id :chat-id} params - request (:request handler-data) - last-clock-value (get-in chats [chat-id :last-clock-value]) - hidden-params (->> (:params command) - (filter :hidden) - (map :name)) - command' (prepare-command current-public-key chat-id now last-clock-value request content) - params' (assoc params :command command') - fx (-> (chat-model/upsert-chat cofx {:chat-id chat-id}) - (update :db add-message-to-db chat-id command' true) - (assoc :save-message (-> command' - (assoc :chat-id chat-id) - (update-in [:content :params] - #(apply dissoc % hidden-params)) - (dissoc :to-message :has-handler :raw-input))))] - (cond-> (merge fx (send cofx params')) - - (:to-message command') - (requests-events/request-answered chat-id (:to-message command')) - - (= constants/console-chat-id chat-id) - (as-> fx' - (let [messages (console-events/console-respond-command-messages params' random-id-seq) - events (mapv #(vector :chat-received-message/add %) messages)] - (update fx' :dispatch-n into events)))))) + [{{:keys [current-public-key chats] :as db} :db :keys [now] :as cofx} params] + (let [{{:keys [handler-data to-message command] :as content} :command chat-id :chat-id} params + request (:request handler-data)] + (handlers/merge-fx cofx + (upsert-and-send (prepare-command-message current-public-key (get chats chat-id) now request content)) + (add-console-responses command handler-data) + (requests-events/request-answered chat-id to-message)))) (defn invoke-console-command-handler [{:keys [db] :as cofx} {:keys [command] :as command-params}] @@ -311,9 +331,9 @@ {:call-jail {:jail-id identity :path [handler-type [name scope-bitmask] :handler] :params jail-params - :callback-events-creator (fn [jail-response] - (when-not (:async-handler command) - [[:command-handler! chat-id orig-params jail-response]]))}})) + :callback-event-creator (fn [jail-response] + (when-not (:async-handler command) + [:command-handler! chat-id orig-params jail-response]))}})) (defn process-command [cofx {:keys [command chat-id] :as params}] diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs index 81cb86569c..34dcc4c7a8 100644 --- a/src/status_im/chat/screen.cljs +++ b/src/status_im/chat/screen.cljs @@ -35,13 +35,11 @@ [react/view style/action [vector-icons/icon :icons/dots-horizontal]]]) -(defview add-contact-bar [] - (letsubs [chat-id [:get-current-chat-id] - pending-contact? [:current-contact :pending?]] - (when (or (nil? pending-contact?) ; user not in contact list - pending-contact?) +(defview add-contact-bar [contact-identity] + (letsubs [{:keys [pending?] :as contact} [:contact-by-identity contact-identity]] + (when (or pending? (not contact)) ;; contact is pending or not in contact list at all [react/touchable-highlight - {:on-press #(re-frame/dispatch [:add-contact chat-id]) + {:on-press #(re-frame/dispatch [:add-contact contact-identity]) :accessibility-label :add-to-contacts-button} [react/view style/add-contact [react/text {:style style/add-contact-text} @@ -52,7 +50,7 @@ :options (actions/actions group-chat? chat-id public?)})) (defview chat-toolbar [public?] - (letsubs [{:keys [group-chat name chat-id]} [:get-current-chat]] + (letsubs [{:keys [group-chat name chat-id contacts]} [:get-current-chat]] [react/view [status-bar/status-bar] [toolbar/platform-agnostic-toolbar {} @@ -62,7 +60,7 @@ :icon-opts {:color :black :accessibility-label :chat-menu-button} :handler #(on-options chat-id name group-chat public?)}]]] - (when-not (or public? group-chat) [add-contact-bar])])) + (when-not (or public? group-chat) [add-contact-bar (-> contacts first :identity)])])) (defmulti message-row (fn [{{:keys [type]} :row}] type)) diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index c267e21d26..21f1a7f59d 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -93,10 +93,11 @@ (defn message-datemark-groups "Transforms map of messages into sequence of `[datemark messages]` tuples, where - messages with particular datemark are sorted according to their `:clock-value` and - tuples themeselves are sorted according to the highest `:clock-value` in the messages." + messages with particular datemark are sorted according to their `:clock-values` and + tuples themeselves are sorted according to the highest `:clock-values` in the messages." [id->messages] - (let [datemark->messages (transduce (comp (map second) + (let [clock-sorter (juxt :from-clock-value :to-clock-value) + datemark->messages (transduce (comp (map second) (filter :show?) (map (fn [{:keys [timestamp] :as msg}] (assoc msg :datemark (time/day-relative timestamp))))) @@ -106,8 +107,9 @@ id->messages)] (->> datemark->messages (map (fn [[datemark messages]] - [datemark (sort-by :clock-value > messages)])) - (sort-by (comp :clock-value first second) >)))) + [datemark (->> messages (sort-by clock-sorter) reverse)])) + (sort-by (comp clock-sorter first second)) + reverse))) (reg-sub :get-chat-message-datemark-groups @@ -330,4 +332,4 @@ :get-chats-unread-messages-number :<- [:get-active-chats] (fn [chats _] - (apply + (map #(count (:unviewed-messages %)) (vals chats))))) \ No newline at end of file + (apply + (map #(count (:unviewed-messages %)) (vals chats))))) diff --git a/src/status_im/chat/views/actions.cljs b/src/status_im/chat/views/actions.cljs index 4a0e7cabcf..8538c325a5 100644 --- a/src/status_im/chat/views/actions.cljs +++ b/src/status_im/chat/views/actions.cljs @@ -16,7 +16,7 @@ (defn- delete-chat [chat-id group?] {:label (i18n/label :t/delete-chat) - :action #(re-frame/dispatch [:delete-chat? chat-id group?])}) + :action #(re-frame/dispatch [:remove-chat-and-navigate-home? chat-id group?])}) (defn- leave-group-chat [chat-id public?] {:label (i18n/label (if public? :t/leave-public-chat :t/leave-group-chat)) diff --git a/src/status_im/chat/views/input/input.cljs b/src/status_im/chat/views/input/input.cljs index c4fc2809b9..5c83f14171 100644 --- a/src/status_im/chat/views/input/input.cljs +++ b/src/status_im/chat/views/input/input.cljs @@ -17,8 +17,7 @@ [status-im.utils.utils :as utils])) (defview basic-text-input [{:keys [set-layout-height-fn set-container-width-fn height single-line-input?]}] - (letsubs [input-text [:chat :input-text] - command [:selected-chat-command] + (letsubs [input-text [:chat :input-text] input-focused? [:get-current-chat-ui-prop :input-focused?] input-ref (atom nil)] [react/text-input @@ -49,10 +48,7 @@ content-size) (set-layout-height-fn (.-height content-size))) (when (not= text input-text) - (re-frame/dispatch [:set-chat-input-text text]) - (when command - (re-frame/dispatch [:load-chat-parameter-box (:command command)])) - (re-frame/dispatch [:update-input-data])))) + (re-frame/dispatch [:set-chat-input-text text])))) :on-content-size-change (when (and (not input-focused?) (not single-line-input?)) #(let [s (.-contentSize (.-nativeEvent %)) @@ -120,8 +116,7 @@ [react/text-input (merge {:ref #(re-frame/dispatch [:set-chat-ui-props {:seq-input-ref %}]) :style (style/seq-input-text command-width container-width) :default-value (or seq-arg-input-text "") - :on-change-text #(do (re-frame/dispatch [:set-chat-seq-arg-input-text %]) - (re-frame/dispatch [:load-chat-parameter-box (:command command)]) + :on-change-text #(do (re-frame/dispatch [:set-chat-seq-arg-input-text %]) (re-frame/dispatch [:set-chat-ui-props {:validation-messages nil}])) :placeholder placeholder :accessibility-label :chat-request-input diff --git a/src/status_im/chat/views/message/message.cljs b/src/status_im/chat/views/message/message.cljs index 70b9d3c1aa..64bfdf2ee1 100644 --- a/src/status_im/chat/views/message/message.cljs +++ b/src/status_im/chat/views/message/message.cljs @@ -58,7 +58,7 @@ (defview message-content-command [{:keys [content params] :as message}] - (letsubs [command [:get-command (:content-command-ref content)]] + (letsubs [command [:get-command (:command-ref content)]] (let [preview (:preview content) {:keys [color] icon-path :icon} command] [react/view style/content-command-view @@ -119,7 +119,7 @@ (fn [text-seq] (map (fn [text] {:text text :url? false}) text-seq)))) -(defn- autolink [string on-press] +(defn- autolink [string event-on-press] (->> (parse-url string) (map-indexed (fn [idx {:keys [text url?]}] (if url? @@ -127,7 +127,7 @@ [react/text {:key idx :style {:color colors/blue} - :on-press #(on-press url)} + :on-press #(re-frame/dispatch [event-on-press url])} url]) text))) vec)) @@ -142,7 +142,7 @@ replacements)) ;; todo rewrite this, naive implementation -(defn- parse-text [string url-on-press] +(defn- parse-text [string event-on-press] (parse-str-regx string regx-styled (fn [text-seq] @@ -157,15 +157,22 @@ (map-indexed (fn [idx string] (apply react/text {:key (str idx "_" string)} - (autolink string url-on-press))) + (autolink string event-on-press))) text-seq)))) +(def cached-parse-text (memoize parse-text)) + (defn text-message [{:keys [content] :as message}] [message-view message - (let [parsed-text (parse-text content #(re-frame/dispatch [:browse-link-from-message %]))] + (let [parsed-text (cached-parse-text content :browse-link-from-message)] [react/text {:style (style/text-message message)} parsed-text])]) +(defn placeholder-message + [{:keys [content] :as message}] + [message-view message + [react/text {:style (style/text-message message)} content]]) + (defmulti message-content (fn [_ message _] (message :content-type))) (defmethod message-content constants/content-type-command-request @@ -190,6 +197,10 @@ [wrapper message [message-view message [message-content-command message]]]) +(defmethod message-content constants/content-type-placeholder + [wrapper message] + [wrapper message [placeholder-message message]]) + (defmethod message-content :default [wrapper {:keys [content-type content] :as message}] [wrapper message @@ -203,7 +214,7 @@ :font :default} (i18n/message-status-label status)]]) -(defview group-message-delivery-status [{:keys [message-id group-id current-public-key user-statuses] :as msg}] +(defview group-message-delivery-status [{:keys [message-id current-public-key user-statuses] :as msg}] (letsubs [{participants :contacts} [:get-current-chat] contacts [:get-contacts]] (let [outgoing-status (or (get user-statuses current-public-key) :sending) @@ -250,14 +261,14 @@ (->> (string/replace photo-path #"contacts://" "") (keyword) (get resources/contacts)) - {:uri (if (string/blank? photo-path) - (identicon/identicon from) - photo-path)}) + {:uri photo-path}) :style style/photo}]]) (defview member-photo [from] (letsubs [photo-path [:get-photo-path from]] - (photo from photo-path))) + (photo from (if (string/blank? photo-path) + (identicon/identicon from) + photo-path)))) (defview my-photo [from] (letsubs [{:keys [photo-path]} [:get-current-account]] @@ -286,8 +297,7 @@ content]] (when last-outgoing? [react/view style/delivery-status - (if (or (= (keyword message-type) :group-user-message) - group-chat) + (if (= message-type :group-user-message) [group-message-delivery-status message] [message-delivery-status message])])]) @@ -296,11 +306,11 @@ (let [to-value @to-value] (when (pos? to-value) (animation/start - (animation/timing val {:toValue to-value - :duration 250}) - (fn [arg] - (when (.-finished arg) - (callback)))))))) + (animation/timing val {:toValue to-value + :duration 250}) + (fn [arg] + (when (.-finished arg) + (callback)))))))) (defn message-container [message & children] (if (:appearing? message) @@ -327,27 +337,14 @@ children)])})) (into [react/view] children))) -(defn chat-message [{:keys [outgoing message-id chat-id from current-public-key] :as message}] - (reagent/create-class - {:display-name - "chat-message" - :component-did-mount - ;; send `:seen` signal when we have signed-in user, message not from us and we didn't sent it already - #(when (and current-public-key message-id chat-id (not outgoing) - (not (models.message/message-seen-by? message current-public-key))) - (re-frame/dispatch [:send-seen! {:chat-id chat-id - :from from - :me current-public-key - :message-id message-id}])) - :reagent-render - (fn [{:keys [outgoing group-chat content-type content] :as message}] - [message-container message - [react/touchable-highlight {:on-press #(when platform/ios? - (react/dismiss-keyboard!)) - :on-long-press #(when (= content-type constants/text-content-type) - (list-selection/share content (i18n/label :t/message)))} - [react/view {:accessibility-label :chat-item} - (let [incoming-group (and group-chat (not outgoing))] - [message-content message-body (merge message - {:current-public-key current-public-key - :incoming-group incoming-group})])]]])})) +(defn chat-message [{:keys [outgoing group-chat current-public-key content-type content] :as message}] + [message-container message + [react/touchable-highlight {:on-press #(when platform/ios? + (react/dismiss-keyboard!)) + :on-long-press #(when (= content-type constants/text-content-type) + (list-selection/share content (i18n/label :t/message)))} + [react/view {:accessibility-label :chat-item} + (let [incoming-group (and group-chat (not outgoing))] + [message-content message-body (merge message + {:current-public-key current-public-key + :incoming-group incoming-group})])]]]) diff --git a/src/status_im/chat/views/message/request_message.cljs b/src/status_im/chat/views/message/request_message.cljs index a2f535be7b..4ccebd0325 100644 --- a/src/status_im/chat/views/message/request_message.cljs +++ b/src/status_im/chat/views/message/request_message.cljs @@ -75,7 +75,7 @@ (defview message-content-command-request [{:keys [message-id content] :as message}] - (letsubs [command [:get-command (:content-command-ref content)] + (letsubs [command [:get-command (:request-command-ref content)] answered? [:is-request-answered? message-id] status-initialized? [:get :status-module-initialized?]] (let [{:keys [prefill prefill-bot-db prefillBotDb params preview] diff --git a/src/status_im/commands/events/loading.cljs b/src/status_im/commands/events/loading.cljs index ebee64c4dd..603d26db91 100644 --- a/src/status_im/commands/events/loading.cljs +++ b/src/status_im/commands/events/loading.cljs @@ -8,16 +8,9 @@ [status-im.utils.utils :as utils] [status-im.utils.config :as config] [status-im.native-module.core :as status] - [status-im.data-store.local-storage :as local-storage] [status-im.bots.events :as bots-events] [taoensso.timbre :as log])) -;; COFX -(re-frame/reg-cofx - :get-local-storage-data - (fn [cofx] - (assoc cofx :get-local-storage-data local-storage/get-data))) - ;; FX (re-frame/reg-fx ::evaluate-jail-n @@ -26,9 +19,9 @@ (status/parse-jail jail-id jail-resource (fn [jail-response] - (let [converted (types/json->clj jail-response)] + (let [converted (types/json->clj jail-response)] (re-frame/dispatch [::proceed-loading jail-id (if config/jsc-enabled? - (update converted :result types/json->clj) + (update converted :result types/json->clj) converted)]))))))) (re-frame/reg-fx @@ -79,7 +72,7 @@ (reduce conj (disj scopes-set scope) (map (partial conj (s/difference scope exclusive-match)) - exclusive-match)) + exclusive-match)) scopes-set))) scopes-set scopes-set)) @@ -138,7 +131,7 @@ (handlers/register-handler-fx ::evaluate-commands-in-jail - [re-frame/trim-v (re-frame/inject-cofx :get-local-storage-data)] + [re-frame/trim-v (re-frame/inject-cofx :data-store/get-local-storage-data)] (fn [cofx [commands-resource whisper-identity]] (evaluate-commands-in-jail cofx commands-resource whisper-identity))) diff --git a/src/status_im/commands/handlers/debug.cljs b/src/status_im/commands/handlers/debug.cljs index 0d84c29a69..20a803ce84 100644 --- a/src/status_im/commands/handlers/debug.cljs +++ b/src/status_im/commands/handlers/debug.cljs @@ -2,8 +2,8 @@ (:require [re-frame.core :as re-frame] [status-im.ui.components.react :as react] [status-im.commands.events.loading :as loading-events] - [status-im.data-store.accounts :as accounts] [status-im.data-store.messages :as messages] + [status-im.data-store.accounts :as accounts] [status-im.utils.handlers :as handlers] [status-im.utils.platform :as platform] [status-im.utils.types :as types] @@ -69,14 +69,14 @@ [{:keys [chats]} {:keys [whisper-identity]}] (if (get chats whisper-identity) (if (get-in chats [whisper-identity :debug?]) - (do (re-frame/dispatch [:remove-chat whisper-identity]) + (do (re-frame/dispatch [:remove-chat-and-navigate-home whisper-identity]) (respond {:type :ok :text "The DApp or bot has been removed."})) (respond {:type :error :text "Your DApp or bot should be debuggable."})) (respond {:type :error :text "There is no such DApp or bot."})) - (re-frame/dispatch [:remove-contact whisper-identity #(and (:dapp? %) (:debug? %))])) + (re-frame/dispatch [:remove-contact whisper-identity])) (defn contact-changed [{:keys [webview-bridge current-chat-id] @@ -177,7 +177,6 @@ ;; TODO(janherich) once `contact-changed` fn is refactored, get rid of this unnecessary event (handlers/register-handler-fx ::load-commands - [re-frame/trim-v (re-frame/inject-cofx :get-local-storage-data)] + [re-frame/trim-v (re-frame/inject-cofx :data-store/get-local-storage-data)] (fn [cofx [contact]] (loading-events/load-commands cofx {} contact))) - diff --git a/src/status_im/commands/handlers/jail.cljs b/src/status_im/commands/handlers/jail.cljs index 5ba4f30acb..7f2699d31b 100644 --- a/src/status_im/commands/handlers/jail.cljs +++ b/src/status_im/commands/handlers/jail.cljs @@ -7,8 +7,7 @@ [status-im.commands.utils :refer [reg-handler]] [status-im.constants :refer [console-chat-id]] [status-im.i18n :refer [get-contact-translated]] - [taoensso.timbre :as log] - [status-im.data-store.local-storage :as local-storage])) + [taoensso.timbre :as log])) (defn command-handler! [_ [chat-id @@ -47,7 +46,7 @@ (defn suggestions-events-handler! [{:keys [bot-db]} [bot-id [n & data] val]] - (log/debug "Suggestion event: " n (first data) val) + (log/debug "Suggestion event: " n (first data) val) (case (keyword n) :set-command-argument (let [[index value move-to-next?] (first data)] @@ -102,8 +101,9 @@ {:result result :chat-id chat-id}]))))) -(reg-handler :set-local-storage - (handlers/side-effect! - (fn [_ [{:keys [data chat-id]}]] - (local-storage/set-data {:chat-id chat-id - :data data})))) +(handlers/register-handler-fx + :set-local-storage + [trim-v] + (fn [_ [{:keys [data chat-id]}]] + {:data-store/set-local-storage-data {:chat-id chat-id + :data data}})) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 65a252836f..f575dc1017 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -15,6 +15,7 @@ (def content-type-command "command") (def content-type-command-request "command-request") (def content-type-status "status") +(def content-type-placeholder "placeholder") (def min-password-length 6) (def max-chat-name-length 20) @@ -28,6 +29,8 @@ (def contract-address "0x0000000000000000000000000000000000000000") +(def system "system") + (def default-wallet-transactions {:filters {:type [{:id :inbound :label (i18n/label :t/incoming) :checked? true} @@ -82,6 +85,7 @@ :DataDir "/ethereum/rinkeby_rpc" :UpstreamConfig {:Enabled true :URL "https://rinkeby.infura.io/z6GCTmjdP3FETEJmMBI4"}}}}) + (def default-networks (transform-config (merge testnet-networks @@ -102,6 +106,9 @@ (def inbox-topic "0xaabb11ee") (def inbox-password "status-offline-inbox") +;; Used to generate topic for contact discoveries +(def contact-discovery "contact-discovery") + (def ^:const send-transaction-no-error-code "0") (def ^:const send-transaction-default-error-code "1") (def ^:const send-transaction-password-error-code "2") diff --git a/src/status_im/data_store/accounts.cljs b/src/status_im/data_store/accounts.cljs index d8bf32b57b..c0da16f2ab 100644 --- a/src/status_im/data_store/accounts.cljs +++ b/src/status_im/data_store/accounts.cljs @@ -1,11 +1,24 @@ (ns status-im.data-store.accounts - (:require [status-im.data-store.realm.accounts :as data-store])) - -(defn get-all [] - (data-store/get-all-as-list)) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.accounts :as data-store])) +;; TODO janherich: define as cofx once debug handlers are refactored (defn get-by-address [address] (data-store/get-by-address address)) -(defn save [account update?] - (data-store/save account update?)) +(re-frame/reg-cofx + :data-store/get-all-accounts + (fn [coeffects _] + (assoc coeffects :all-accounts (data-store/get-all-as-list)))) + +(re-frame/reg-fx + :data-store/save-account + (fn [{:keys [after-update-event] :as account}] + (let [account-to-save (dissoc account :after-update-event)] + (async/go (async/>! core/realm-queue #(if after-update-event + (do (data-store/save account-to-save true) + (re-frame/dispatch after-update-event)) + (data-store/save account-to-save true))))))) + diff --git a/src/status_im/data_store/browser.cljs b/src/status_im/data_store/browser.cljs index ed399f8b2b..0beeb999dc 100644 --- a/src/status_im/data_store/browser.cljs +++ b/src/status_im/data_store/browser.cljs @@ -1,23 +1,21 @@ (ns status-im.data-store.browser - (:require [status-im.data-store.realm.browser :as data-store]) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.browser :as data-store]) (:refer-clojure :exclude [exists?])) -(defn get-all - [] - (data-store/get-all)) +(re-frame/reg-cofx + :data-store/all-browsers + (fn [cofx _] + (assoc cofx :all-stored-browsers (data-store/get-all)))) -(defn get-by-id - [id] - (data-store/get-by-id id)) +(re-frame/reg-fx + :data-store/save-browser + (fn [{:keys [browser-id] :as browser}] + (async/go (async/>! core/realm-queue #(data-store/save browser (data-store/exists? browser-id)))))) -(defn exists? - [browser-id] - (data-store/exists? browser-id)) - -(defn save - [{:keys [browser-id] :as browser}] - (data-store/save browser (exists? browser-id))) - -(defn delete - [browser-id] - (data-store/delete browser-id)) \ No newline at end of file +(re-frame/reg-fx + :data-store/remove-browser + (fn [browser-id] + (async/go (async/>! core/realm-queue #(data-store/delete browser-id))))) diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index f98cb58cd6..86d29b9de8 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -1,63 +1,51 @@ (ns status-im.data-store.chats - (:require [status-im.data-store.realm.chats :as data-store]) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.chats :as data-store]) (:refer-clojure :exclude [exists?])) -(defn get-all - [] - (data-store/get-all-active)) +(re-frame/reg-cofx + :data-store/all-chats + (fn [cofx _] + (assoc cofx :all-stored-chats (data-store/get-all-active)))) -(defn get-inactive-ids - [] - (data-store/get-inactive-ids)) +(re-frame/reg-cofx + :data-store/inactive-chat-ids + (fn [cofx _] + (assoc cofx :inactive-chat-ids (data-store/get-inactive-ids)))) -(defn get-by-id - [id] - (data-store/get-by-id id)) +(re-frame/reg-cofx + :data-store/get-chat + (fn [cofx _] + (assoc cofx :get-stored-chat data-store/get-by-id))) -(defn exists? - [chat-id] - (data-store/exists? chat-id)) +(re-frame/reg-fx + :data-store/save-chat + (fn [{:keys [chat-id] :as chat}] + (async/go (async/>! core/realm-queue #(data-store/save chat (data-store/exists? chat-id)))))) -(defn save - [{:keys [chat-id] :as chat}] - (data-store/save chat (data-store/exists? chat-id))) +(re-frame/reg-fx + :data-store/delete-chat + (fn [chat-id] + (async/go (async/>! core/realm-queue #(data-store/delete chat-id))))) -(defn delete - [chat-id] - (data-store/delete chat-id)) +(re-frame/reg-fx + :data-store/deactivate-chat + (fn [chat-id] + (async/go (async/>! core/realm-queue #(data-store/set-inactive chat-id))))) -(defn set-inactive - [chat-id] - (data-store/set-inactive chat-id)) +(re-frame/reg-fx + :data-store/add-chat-contacts + (fn [[chat-id contacts]] + (async/go (async/>! core/realm-queue #(data-store/add-contacts chat-id contacts))))) -(defn get-contacts - [chat-id] - (data-store/get-contacts chat-id)) +(re-frame/reg-fx + :data-store/remove-chat-contacts + (fn [[chat-id contacts]] + (async/go (async/>! core/realm-queue #(data-store/remove-contacts chat-id contacts))))) -(defn add-contacts - [chat-id identities] - (data-store/add-contacts chat-id identities)) - -(defn remove-contacts - [chat-id identities] - (data-store/remove-contacts chat-id identities)) - -(defn save-property - [chat-id property-name value] - (data-store/save-property chat-id property-name value)) - -(defn get-property - [chat-id property-name] - (data-store/get-property chat-id property-name)) - -(defn removed-at - [chat-id] - (get-property chat-id :removed-at)) - -(defn get-active-group-chats - [] - (data-store/get-active-group-chats)) - -(defn set-active - [chat-id active?] - (save-property chat-id :is-active active?)) +(re-frame/reg-fx + :data-store/save-chat-property + (fn [[chat-id prop value]] + (async/go (async/>! core/realm-queue #(data-store/save-property chat-id prop value))))) diff --git a/src/status_im/data_store/contact_groups.cljs b/src/status_im/data_store/contact_groups.cljs index 5d45cd6ddc..cc66bc6396 100644 --- a/src/status_im/data_store/contact_groups.cljs +++ b/src/status_im/data_store/contact_groups.cljs @@ -1,32 +1,38 @@ (ns status-im.data-store.contact-groups - (:require [status-im.data-store.realm.contact-groups :as data-store]) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.contact-groups :as data-store]) (:refer-clojure :exclude [exists?])) (defn- normalize-contacts [item] (update item :contacts vals)) -(defn get-all - [] - (map normalize-contacts (data-store/get-all-as-list))) +(re-frame/reg-cofx + :data-store/get-all-contact-groups + (fn [cofx _] + (assoc cofx :all-contact-groups (into {} + (map (comp (juxt :group-id identity) normalize-contacts)) + (data-store/get-all-as-list))))) -(defn save - [{:keys [group-id] :as group}] - (data-store/save group (data-store/exists? group-id))) +(re-frame/reg-fx + :data-store/save-contact-group + (fn [{:keys [group-id] :as group}] + (async/go (async/>! core/realm-queue #(data-store/save group (data-store/exists? group-id)))))) -(defn save-all - [groups] - (mapv save groups)) +(re-frame/reg-fx + :data-store/save-contact-groups + (fn [groups] + (doseq [{:keys [group-id] :as group} groups] + (async/go (async/>! core/realm-queue #(data-store/save group (data-store/exists? group-id))))))) -(defn save-property - [group-id property-name value] - (data-store/save-property group-id property-name value)) - -(defn delete - [group-id] - (data-store/delete group-id)) - -(defn add-contacts - [group-id identities] - (data-store/add-contacts group-id identities)) +(re-frame/reg-fx + :data-store/save-contact-group-property + (fn [[group-id property-name value]] + (async/go (async/>! core/realm-queue #(data-store/save-property group-id property-name value))))) +(re-frame/reg-fx + :data-store/add-contacts-to-contact-group + (fn [[group-id contacts]] + (async/go (async/>! core/realm-queue #(data-store/add-contacts group-id contacts))))) diff --git a/src/status_im/data_store/contacts.cljs b/src/status_im/data_store/contacts.cljs index 28239deb04..7a91e0a47d 100644 --- a/src/status_im/data_store/contacts.cljs +++ b/src/status_im/data_store/contacts.cljs @@ -1,16 +1,20 @@ (ns status-im.data-store.contacts - (:require [status-im.data-store.realm.contacts :as data-store]) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.contacts :as data-store]) (:refer-clojure :exclude [exists?])) -(defn get-all - [] - (data-store/get-all-as-list)) +(re-frame/reg-cofx + :data-store/get-all-contacts + (fn [coeffects _] + (assoc coeffects :all-contacts (data-store/get-all-as-list)))) -(defn get-by-id +(defn- get-by-id [whisper-identity] (data-store/get-by-id-cljs whisper-identity)) -(defn save +(defn- save [{:keys [whisper-identity pending?] :as contact}] (let [{pending-db? :pending? :as contact-db} (get-by-id whisper-identity) @@ -21,13 +25,18 @@ (dissoc :command :response :subscriptions :jail-loaded-events))] (data-store/save contact' (boolean contact-db)))) -(defn save-all - [contacts] - (mapv save contacts)) +(re-frame/reg-fx + :data-store/save-contact + (fn [contact] + (async/go (async/>! core/realm-queue #(save contact))))) -(defn delete [contact] - (data-store/delete contact)) +(re-frame/reg-fx + :data-store/save-contacts + (fn [contacts] + (doseq [contact contacts] + (async/go (async/>! core/realm-queue #(save contact)))))) -(defn exists? - [whisper-identity] - (data-store/exists? whisper-identity)) +(re-frame/reg-fx + :data-store/delete-contact + (fn [contact] + (async/go (async/>! core/realm-queue #(data-store/delete contact))))) diff --git a/src/status_im/data_store/core.cljs b/src/status_im/data_store/core.cljs index 471610022a..91f316fdba 100644 --- a/src/status_im/data_store/core.cljs +++ b/src/status_im/data_store/core.cljs @@ -1,5 +1,14 @@ (ns status-im.data-store.core - (:require [status-im.data-store.realm.core :as data-source] + (:require status-im.data-store.chats + status-im.data-store.messages + status-im.data-store.contacts + status-im.data-store.transport + status-im.data-store.browser + status-im.data-store.accounts + status-im.data-store.local-storage + status-im.data-store.contact-groups + status-im.data-store.requests + [status-im.data-store.realm.core :as data-source] [status-im.utils.handlers :as handlers])) diff --git a/src/status_im/data_store/discover.cljs b/src/status_im/data_store/discover.cljs index 2d7d22a1e3..8d60d7f948 100644 --- a/src/status_im/data_store/discover.cljs +++ b/src/status_im/data_store/discover.cljs @@ -10,7 +10,7 @@ ;; also deletes the oldest queries if the number of discovers stored is ;; above maximum-number-of-discoveries (re-frame/reg-fx - :data-store.discover/save-all + :data-store/save-all-discoveries (fn [[discovers maximum-number-of-discoveries]] (data-store/save-all (mapv #(dissoc % :tags) discovers)) (data-store/delete :created-at :asc maximum-number-of-discoveries))) diff --git a/src/status_im/data_store/local_storage.cljs b/src/status_im/data_store/local_storage.cljs index 29d9324068..7543c49a18 100644 --- a/src/status_im/data_store/local_storage.cljs +++ b/src/status_im/data_store/local_storage.cljs @@ -1,9 +1,15 @@ (ns status-im.data-store.local-storage - (:require [status-im.data-store.realm.local-storage :as data-store])) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.local-storage :as data-store])) +(re-frame/reg-cofx + :data-store/get-local-storage-data + (fn [cofx _] + (assoc cofx :get-local-storage-data (comp :data data-store/get-by-chat-id)))) -(defn get-data [chat-id] - (:data (data-store/get-by-chat-id chat-id))) - -(defn set-data [local-storage] - (data-store/save local-storage)) +(re-frame/reg-fx + :data-store/set-local-storage-data + (fn [data] + (async/go (async/>! core/realm-queue #(data-store/save data))))) diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 0405ca95e8..9fa8c31885 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -1,12 +1,21 @@ (ns status-im.data-store.messages - (:refer-clojure :exclude [exists?]) (:require [cljs.reader :as reader] + [cljs.core.async :as async] + [re-frame.core :as re-frame] [status-im.constants :as constants] + [status-im.data-store.realm.core :as core] [status-im.data-store.realm.messages :as data-store] [status-im.utils.random :as random] [status-im.utils.core :as utils] [status-im.utils.datetime :as datetime])) +;; TODO janherich: define as cofx once debug handlers are refactored +(defn get-log-messages + [chat-id] + (->> (data-store/get-by-chat-id chat-id 0 100) + (filter #(= (:content-type %) constants/content-type-log-message)) + (map #(select-keys % [:content :timestamp])))) + (defn- command-type? [type] (contains? @@ -17,9 +26,10 @@ {:outgoing false :to nil}) -(defn get-by-id - [message-id] - (data-store/get-by-id message-id)) +(re-frame/reg-cofx + :data-store/get-message + (fn [cofx _] + (assoc cofx :get-stored-message data-store/get-by-id))) (defn get-by-chat-id ([chat-id] @@ -31,22 +41,25 @@ (update message :content reader/read-string) message)))))) -(defn get-stored-message-ids - [] - (data-store/get-stored-message-ids)) +(re-frame/reg-cofx + :data-store/get-messages + (fn [cofx _] + (assoc cofx :get-stored-messages get-by-chat-id))) -(defn get-log-messages - [chat-id] - (->> (data-store/get-by-chat-id chat-id 0 100) - (filter #(= (:content-type %) constants/content-type-log-message)) - (map #(select-keys % [:content :timestamp])))) +(re-frame/reg-cofx + :data-store/message-ids + (fn [cofx _] + (assoc cofx :stored-message-ids (data-store/get-stored-message-ids)))) -(defn get-unviewed - [current-public-key] - (into {} - (map (fn [[chat-id user-statuses]] - [chat-id (into #{} (map :message-id) user-statuses)])) - (group-by :chat-id (data-store/get-unviewed current-public-key)))) +(re-frame/reg-cofx + :data-store/unviewed-messages + (fn [{:keys [db] :as cofx} _] + (assoc cofx + :stored-unviewed-messages + (into {} + (map (fn [[chat-id user-statuses]] + [chat-id (into #{} (map :message-id) user-statuses)])) + (group-by :chat-id (data-store/get-unviewed (:current-public-key db))))))) (defn- prepare-content [content] (if (string? content) @@ -79,10 +92,35 @@ {:from (or from "anonymous") :timestamp (datetime/timestamp)}))))) +(re-frame/reg-fx + :data-store/save-message + (fn [message] + (async/go (async/>! core/realm-queue #(save message))))) + (defn update-message [{:keys [message-id] :as message}] (when-let [{:keys [chat-id]} (data-store/get-by-id message-id)] (data-store/save (prepare-message (assoc message :chat-id chat-id))))) -(defn delete-by-chat-id [chat-id] - (data-store/delete-by-chat-id chat-id)) +(re-frame/reg-fx + :data-store/update-message + (fn [message] + (async/go (async/>! core/realm-queue #(update-message message))))) + +(re-frame/reg-fx + :data-store/update-messages + (fn [messages] + (doseq [message messages] + (async/go (async/>! core/realm-queue #(update-message message)))))) + +(re-frame/reg-fx + :data-store/delete-messages + (fn [chat-id] + (async/go (async/>! core/realm-queue #(data-store/delete-by-chat-id chat-id))))) + +(re-frame/reg-fx + :data-store/hide-messages + (fn [chat-id] + (async/go (async/>! core/realm-queue #(doseq [message-id (data-store/get-message-ids-by-chat-id chat-id)] + (data-store/save {:message-id message-id + :show? false})))))) diff --git a/src/status_im/data_store/networks.cljs b/src/status_im/data_store/networks.cljs deleted file mode 100644 index de272c1658..0000000000 --- a/src/status_im/data_store/networks.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.data-store.networks - (:require [status-im.data-store.realm.networks :as data-store])) - -(defn get-all - [] - (data-store/get-all-as-list)) - -(defn save - [{:keys [id] :as network}] - (data-store/save network (data-store/exists? id))) - -(defn save-all - [networks] - (mapv save networks)) - -(defn save-property - [id property-name value] - (data-store/save-property id property-name value)) - -(defn delete - [id] - (data-store/delete id)) diff --git a/src/status_im/data_store/pending_messages.cljs b/src/status_im/data_store/pending_messages.cljs deleted file mode 100644 index 9adb15a004..0000000000 --- a/src/status_im/data_store/pending_messages.cljs +++ /dev/null @@ -1,49 +0,0 @@ -(ns status-im.data-store.pending-messages - (:require [status-im.data-store.realm.pending-messages :as data-store] - [status-im.utils.hex :as i])) - -(defn- get-id - [message-id to] - (let [to' (i/normalize-hex to) - to'' (when to' (subs to' 0 7)) - id' (if to'' - (str message-id "-" (subs to'' 0 7)) - message-id)] - id')) - -(defn get-all - [] - (data-store/get-all-as-list)) - -(defn get-by-chat-id - [chat-id] - (data-store/get-by-chat-id chat-id)) - -(defn get-by-message-id - [message-id] - (data-store/get-by-message-id message-id)) - -(defn save - [{:keys [id to group-id message] :as pending-message}] - (let [{:keys [sig sym-key-password pubKey topic payload]} message - id' (get-id id to) - chat-id (or group-id to) - message' (-> pending-message - (assoc :id id' - :sig sig - :sym-key-password sym-key-password - :pub-key pubKey - :message-id id - :chat-id chat-id - :payload payload - :topic topic) - (dissoc :message))] - (data-store/save message'))) - -(defn delete - [message-id] - (data-store/delete message-id)) - -(defn delete-all-by-chat-id - [chat-id] - (data-store/delete-all-by-chat-id chat-id)) diff --git a/src/status_im/data_store/processed_messages.cljs b/src/status_im/data_store/processed_messages.cljs deleted file mode 100644 index ce40a9bf8e..0000000000 --- a/src/status_im/data_store/processed_messages.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns status-im.data-store.processed-messages - (:require [status-im.data-store.realm.processed-messages :as data-store]) - (:refer-clojure :exclude [exists?])) - -(defn get-filtered - [condition] - (data-store/get-filtered-as-list condition)) - -(defn save - [processed-message] - (data-store/save processed-message)) - -(defn delete [condition] - (data-store/delete condition)) diff --git a/src/status_im/data_store/realm/chats.cljs b/src/status_im/data_store/realm/chats.cljs index 4e9da07f6f..7217d92027 100644 --- a/src/status_im/data_store/realm/chats.cljs +++ b/src/status_im/data_store/realm/chats.cljs @@ -7,10 +7,12 @@ (:refer-clojure :exclude [exists?])) (defn- normalize-chat [{:keys [chat-id] :as chat}] - (let [last-message (messages/get-last-message chat-id)] + (let [last-to-clock-value (messages/get-last-clock-value chat-id :to-clock-value) + last-from-clock-value (messages/get-last-clock-value chat-id :from-clock-value)] (-> chat (realm/fix-map->vec :contacts) - (assoc :last-clock-value (or (:clock-value last-message) 0))))) + (merge {:last-to-clock-value (or last-to-clock-value 0) + :last-from-clock-value (or last-from-clock-value 0)})))) (defn get-all-active [] diff --git a/src/status_im/data_store/realm/core.cljs b/src/status_im/data_store/realm/core.cljs index 9696d51177..aede2d9f8a 100644 --- a/src/status_im/data_store/realm/core.cljs +++ b/src/status_im/data_store/realm/core.cljs @@ -4,6 +4,7 @@ [status-im.data-store.realm.schemas.base.core :as base] [taoensso.timbre :as log] [status-im.utils.fs :as fs] + [status-im.utils.async :as utils.async] [clojure.string :as str] [goog.string :as gstr] [cognitect.transit :as transit] @@ -43,6 +44,8 @@ (def account-realm (atom (open-migrated-realm new-account-filename account/schemas))) +(def realm-queue (utils.async/task-queue 2000)) + (defn close-account-realm [] (close @account-realm) (reset! account-realm nil)) diff --git a/src/status_im/data_store/realm/messages.cljs b/src/status_im/data_store/realm/messages.cljs index a68857ced6..6294a5b2ac 100644 --- a/src/status_im/data_store/realm/messages.cljs +++ b/src/status_im/data_store/realm/messages.cljs @@ -32,6 +32,12 @@ realm/js-object->clj)] (mapv transform-message messages)))) +(defn get-message-ids-by-chat-id + [chat-id] + (.map (realm/get-by-field @realm/account-realm :message :chat-id chat-id) + (fn [msg _ _] + (aget msg "message-id")))) + (defn get-stored-message-ids [] (let [chat-id->message-id (volatile! {})] @@ -52,11 +58,12 @@ (realm/page from (+ from number-of-messages)) realm/js-object->clj)) -(defn get-last-message - [chat-id] +(defn get-last-clock-value + [chat-id clock-prop] (-> (realm/get-by-field @realm/account-realm :message :chat-id chat-id) - (realm/sorted :clock-value :desc) - (realm/single-clj))) + (realm/sorted clock-prop :desc) + (realm/single-clj) + (get clock-prop))) (defn get-unviewed [current-public-key] diff --git a/src/status_im/data_store/realm/networks.cljs b/src/status_im/data_store/realm/networks.cljs deleted file mode 100644 index c8a4716443..0000000000 --- a/src/status_im/data_store/realm/networks.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.data-store.realm.networks - (:require [status-im.data-store.realm.core :as realm]) - (:refer-clojure :exclude [exists?])) - -(defn get-all - [] - (-> @realm/account-realm - (realm/get-all :network))) - -(defn get-all-as-list - [] - (realm/js-object->clj (get-all))) - -(defn save - [network update?] - (realm/save @realm/account-realm :network network update?)) - -(defn save-property - [id property-name value] - (realm/write @realm/account-realm - (fn [] - (-> @realm/account-realm - (realm/get-one-by-field :network :id id) - (aset (name property-name) value))))) - -(defn exists? - [id] - (realm/exists? @realm/account-realm :network {:id id})) - -(defn delete - [id] - (when-let [network (realm/get-one-by-field @realm/account-realm :network :id id)] - (realm/delete @realm/account-realm network))) diff --git a/src/status_im/data_store/realm/pending_messages.cljs b/src/status_im/data_store/realm/pending_messages.cljs deleted file mode 100644 index 21d787b4a1..0000000000 --- a/src/status_im/data_store/realm/pending_messages.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.data-store.realm.pending-messages - (:require [status-im.data-store.realm.core :as realm] - [cljs.reader :refer [read-string]])) - -(defn get-all - [] - (realm/get-all @realm/account-realm :pending-message)) - -(defn get-all-as-list - [] - (realm/js-object->clj (get-all))) - -(defn get-by-message-id - [message-id] - (realm/get-by-field @realm/account-realm :pending-message :message-id message-id)) - -(defn get-by-chat-id - [chat-id] - (realm/get-by-field @realm/account-realm :pending-message :chat-id chat-id)) - -(defn save - [pending-message] - (realm/save @realm/account-realm :pending-message pending-message true)) - -(defn delete - [message-id] - (realm/delete @realm/account-realm (get-by-message-id message-id))) - -(defn delete-all-by-chat-id - [chat-id] - (realm/delete @realm/account-realm (get-by-chat-id chat-id))) diff --git a/src/status_im/data_store/realm/processed_messages.cljs b/src/status_im/data_store/realm/processed_messages.cljs deleted file mode 100644 index 52dbb85714..0000000000 --- a/src/status_im/data_store/realm/processed_messages.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns status-im.data-store.realm.processed-messages - (:require [status-im.data-store.realm.core :as realm]) - (:refer-clojure :exclude [exists?])) - -(defn get-all - [] - (-> @realm/account-realm - (realm/get-all :processed-message) - (realm/sorted :ttl :asc))) - -(defn get-filtered - [condition] - (realm/filtered (get-all) condition)) - -(defn get-filtered-as-list - [condition] - (realm/js-object->clj (get-filtered condition))) - -(defn save - [processed-message] - (realm/save @realm/account-realm :processed-message processed-message)) - -(defn delete - [condition] - (realm/delete @realm/account-realm (get-filtered condition))) diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs index b490dceecb..981db10565 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -20,7 +20,8 @@ [status-im.data-store.realm.schemas.account.v19.core :as v19] [status-im.data-store.realm.schemas.account.v20.core :as v20] [status-im.data-store.realm.schemas.account.v21.core :as v21] - [status-im.data-store.realm.schemas.account.v22.core :as v22])) + [status-im.data-store.realm.schemas.account.v22.core :as v22] + [status-im.data-store.realm.schemas.account.v23.core :as v23])) ;; TODO(oskarth): Add failing test if directory vXX exists but isn't in schemas. @@ -91,5 +92,7 @@ :migration v21/migration} {:schema v22/schema :schemaVersion 22 - :migration v22/migration}]) - + :migration v22/migration} + {:schema v23/schema + :schemaVersion 23 + :migration v23/migration}]) diff --git a/src/status_im/data_store/realm/schemas/account/v22/core.cljs b/src/status_im/data_store/realm/schemas/account/v22/core.cljs index aa14142705..25d05bdaac 100644 --- a/src/status_im/data_store/realm/schemas/account/v22/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/v22/core.cljs @@ -1,5 +1,6 @@ (ns status-im.data-store.realm.schemas.account.v22.core (:require [status-im.data-store.realm.schemas.account.v22.chat :as chat] + [status-im.data-store.realm.schemas.account.v22.transport :as transport] [status-im.data-store.realm.schemas.account.v1.chat-contact :as chat-contact] [status-im.data-store.realm.schemas.account.v19.contact :as contact] [status-im.data-store.realm.schemas.account.v20.discover :as discover] @@ -18,6 +19,7 @@ (def schema [chat/schema chat-contact/schema + transport/schema contact/schema discover/schema message/schema diff --git a/src/status_im/data_store/realm/schemas/account/v22/transport.cljs b/src/status_im/data_store/realm/schemas/account/v22/transport.cljs new file mode 100644 index 0000000000..363c0222f8 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v22/transport.cljs @@ -0,0 +1,15 @@ +(ns status-im.data-store.realm.schemas.account.v22.transport) + +(def schema {:name :transport + :primaryKey :chat-id + :properties {:chat-id :string + :ack :string + :seen :string + :pending-ack :string + :pending-send :string + :topic :string + :sym-key-id {:type :string + :optional true} + ;;TODO (yenda) remove once go implements persistence + :sym-key {:type :string + :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v23/core.cljs b/src/status_im/data_store/realm/schemas/account/v23/core.cljs new file mode 100644 index 0000000000..5214c3cb44 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v23/core.cljs @@ -0,0 +1,62 @@ +(ns status-im.data-store.realm.schemas.account.v23.core + (:require [status-im.data-store.realm.schemas.account.v22.chat :as chat] + [status-im.data-store.realm.schemas.account.v22.transport :as transport] + [status-im.data-store.realm.schemas.account.v1.chat-contact :as chat-contact] + [status-im.data-store.realm.schemas.account.v19.contact :as contact] + [status-im.data-store.realm.schemas.account.v20.discover :as discover] + [status-im.data-store.realm.schemas.account.v23.message :as message] + [status-im.data-store.realm.schemas.account.v19.request :as request] + [status-im.data-store.realm.schemas.account.v19.user-status :as user-status] + [status-im.data-store.realm.schemas.account.v5.contact-group :as contact-group] + [status-im.data-store.realm.schemas.account.v5.group-contact :as group-contact] + [status-im.data-store.realm.schemas.account.v8.local-storage :as local-storage] + [status-im.data-store.realm.schemas.account.v21.browser :as browser] + [goog.object :as object] + [taoensso.timbre :as log] + [cljs.reader :as reader] + [clojure.string :as string])) + +(def schema [chat/schema + chat-contact/schema + transport/schema + contact/schema + discover/schema + message/schema + request/schema + user-status/schema + contact-group/schema + group-contact/schema + local-storage/schema + browser/schema]) + +(defn update-new-message [new-realm message-id to-clock-value from-clock-value] + (when-let [message (some-> new-realm + (.objects "message") + (.filtered (str "message-id = \"" message-id "\"")) + (aget 0))] + (aset message "to-clock-value" to-clock-value) + (aset message "from-clock-value" from-clock-value))) + +(defn update-chat-messages [old-realm new-realm chat-id] + (let [from-clock-value (atom 0) + to-clock-value (atom 0)] + (some-> old-realm + (.objects "message") + (.filtered (str "chat-id = \"" chat-id "\"")) + (.sorted "clock-value" false) + (.map (fn [message _ _] + (let [message-id (object/get message "message-id") + outgoing? (boolean (object/get message "outgoing"))] + (if outgoing? + (update-new-message new-realm message-id (swap! to-clock-value inc) @from-clock-value) + (update-new-message new-realm message-id @to-clock-value (swap! from-clock-value inc))))))))) + +(defn update-chats [old-realm new-realm] + (some-> new-realm + (.objects "chat") + (.map (fn [chat _ _] + (update-chat-messages old-realm new-realm (object/get chat "chat-id")))))) + +(defn migration [old-realm new-realm] + (log/debug "migrating v23 account database: " old-realm new-realm) + (update-chats old-realm new-realm)) diff --git a/src/status_im/data_store/realm/schemas/account/v23/message.cljs b/src/status_im/data_store/realm/schemas/account/v23/message.cljs new file mode 100644 index 0000000000..3cf5776c8c --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v23/message.cljs @@ -0,0 +1,30 @@ +(ns status-im.data-store.realm.schemas.account.v23.message) + +(def schema {:name :message + :primaryKey :message-id + :properties {:message-id :string + :from :string + :to {:type :string + :optional true} + :content :string ; TODO make it ArrayBuffer + :content-type :string + :username {:type :string + :optional true} + :timestamp :int + :chat-id {:type :string + :indexed true} + :outgoing :bool + :retry-count {:type :int + :default 0} + :message-type {:type :string + :optional true} + :message-status {:type :string + :optional true} + :user-statuses {:type :list + :objectType :user-status} + :from-clock-value {:type :int + :default 0} + :to-clock-value {:type :int + :default 0} + :show? {:type :bool + :default true}}}) diff --git a/src/status_im/data_store/realm/transport.cljs b/src/status_im/data_store/realm/transport.cljs new file mode 100644 index 0000000000..68120d98ce --- /dev/null +++ b/src/status_im/data_store/realm/transport.cljs @@ -0,0 +1,21 @@ +(ns status-im.data-store.realm.transport + (:require [status-im.data-store.realm.core :as realm]) + (:refer-clojure :exclude [exists?])) + +(defn get-all + [] + (-> (realm/get-all @realm/account-realm :transport) + realm/js-object->clj)) + +(defn exists? + [chat-id] + (realm/exists? @realm/account-realm :transport {:chat-id chat-id})) + +(defn save + [{:keys [chat-id] :as chat}] + (realm/save @realm/account-realm :transport chat (exists? chat-id))) + +(defn delete + [chat-id] + (when-let [chat (realm/get-by-field @realm/account-realm :transport :chat-id chat-id)] + (realm/delete @realm/account-realm chat))) diff --git a/src/status_im/data_store/requests.cljs b/src/status_im/data_store/requests.cljs index 6582b0afb3..f91215f00b 100644 --- a/src/status_im/data_store/requests.cljs +++ b/src/status_im/data_store/requests.cljs @@ -1,14 +1,20 @@ (ns status-im.data-store.requests - (:require [status-im.data-store.realm.requests :as data-store])) + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.core :as core] + [status-im.data-store.realm.requests :as data-store])) -(defn get-all-unanswered - [] - (data-store/get-all-unanswered)) +(re-frame/reg-cofx + :data-store/get-unanswered-requests + (fn [cofx _] + (assoc cofx :stored-unanswered-requests (data-store/get-all-unanswered)))) -(defn save - [request] - (data-store/save request)) +(re-frame/reg-fx + :data-store/save-request + (fn [request] + (async/go (async/>! core/realm-queue #(data-store/save request))))) -(defn mark-as-answered - [chat-id message-id] - (data-store/mark-as-answered chat-id message-id)) +(re-frame/reg-fx + :data-store/mark-request-as-answered + (fn [{:keys [chat-id message-id]}] + (async/go (async/>! core/realm-queue #(data-store/mark-as-answered chat-id message-id))))) diff --git a/src/status_im/data_store/transport.cljs b/src/status_im/data_store/transport.cljs new file mode 100644 index 0000000000..3e76519ab3 --- /dev/null +++ b/src/status_im/data_store/transport.cljs @@ -0,0 +1,44 @@ +(ns status-im.data-store.transport + (:require [cljs.tools.reader.edn :as edn] + [cljs.core.async :as async] + [re-frame.core :as re-frame] + [status-im.data-store.realm.transport :as data-store] + [status-im.data-store.realm.core :as core])) + + +(defn deserialize-chat [serialized-chat] + (-> serialized-chat + (dissoc :chat-id) + (update :ack edn/read-string) + (update :seen edn/read-string) + (update :pending-ack edn/read-string) + (update :pending-send edn/read-string))) + +(re-frame/reg-cofx + :data-store/transport + (fn [cofx _] + (assoc cofx + :data-store/transport + (reduce (fn [acc {:keys [chat-id] :as chat}] + (assoc acc chat-id (deserialize-chat chat))) + {} + (data-store/get-all))))) + +(defn save [chat-id chat] + (let [serialized-chat (-> chat + (assoc :chat-id chat-id) + (update :ack pr-str) + (update :seen pr-str) + (update :pending-ack pr-str) + (update :pending-send pr-str))] + (data-store/save serialized-chat))) + +(re-frame/reg-fx + :data-store.transport/save + (fn [{:keys [chat-id chat]}] + (async/go (async/>! core/realm-queue #(save chat-id chat))))) + +(re-frame/reg-fx + :data-store.transport/delete + (fn [chat-id] + (async/go (async/>! core/realm-queue #(data-store/delete chat-id))))) diff --git a/src/status_im/protocol/ack.cljs b/src/status_im/protocol/ack.cljs deleted file mode 100644 index c77320c448..0000000000 --- a/src/status_im/protocol/ack.cljs +++ /dev/null @@ -1,24 +0,0 @@ -(ns status-im.protocol.ack - (:require [status-im.protocol.web3.delivery :as d] - [status-im.protocol.web3.filtering :as f] - [status-im.utils.random :as random])) - -(defn check-ack! - [web3 - from - {:keys [type requires-ack? message-id ack? group-id ack-of-message]} - identity] - (when (and requires-ack? (not ack?)) - (let [message {:from identity - :to from - :message-id (random/id) - :topics [f/status-topic] - :type type - :ack? true - :payload {:type type - :ack? true - :ack-of-message message-id - :group-id group-id}}] - (d/add-pending-message! web3 message))) - (when ack? - (d/remove-pending-message! web3 ack-of-message from))) diff --git a/src/status_im/protocol/chat.cljs b/src/status_im/protocol/chat.cljs deleted file mode 100644 index a6726e2f4f..0000000000 --- a/src/status_im/protocol/chat.cljs +++ /dev/null @@ -1,43 +0,0 @@ -(ns status-im.protocol.chat - (:require [cljs.spec.alpha :as s] - [status-im.protocol.web3.filtering :as f] - [status-im.protocol.web3.delivery :as d] - [taoensso.timbre :refer-macros [debug] :as log] - [status-im.protocol.validation :refer-macros [valid?]])) - -(def message-defaults - {:topics [f/status-topic]}) - -(s/def ::timestamp int?) -(s/def ::user-message - (s/merge - :protocol/message - (s/keys :req-un [:message/to :chat-message/payload]))) - -(defn send! - [{:keys [web3 message]}] - {:pre [(valid? ::user-message message)]} - (let [topics (f/get-topics (:to message)) - message' (assoc message - :topics topics - :type :message - :requires-ack? true)] - (debug :send-user-message message') - (d/add-pending-message! web3 message'))) - -(s/def ::seen-message - (s/merge :protocol/message (s/keys :req-un [:message/to]))) - -(defn send-seen! - [{:keys [web3 message]}] - {:pre [(valid? ::seen-message message)]} - (debug :send-seen message) - (d/add-pending-message! - web3 - (merge message-defaults - (-> message - (assoc - :type :seen - :requires-ack? false) - (assoc-in [:payload :group-id] (:group-id message)) - (dissoc :group-id))))) diff --git a/src/status_im/protocol/core.cljs b/src/status_im/protocol/core.cljs deleted file mode 100644 index 0df4418ca6..0000000000 --- a/src/status_im/protocol/core.cljs +++ /dev/null @@ -1,125 +0,0 @@ -(ns status-im.protocol.core - (:require status-im.protocol.message - [status-im.protocol.web3.utils :as u] - [status-im.protocol.web3.filtering :as f] - [status-im.protocol.web3.delivery :as d] - [status-im.protocol.web3.inbox :as inbox] - [taoensso.timbre :refer-macros [debug] :as log] - [status-im.protocol.validation :refer-macros [valid?]] - [status-im.protocol.web3.keys :as shh-keys] - [status-im.protocol.chat :as chat] - [status-im.protocol.group :as group] - [status-im.protocol.listeners :as l] - [status-im.protocol.encryption :as e] - [status-im.protocol.discoveries :as discoveries] - [cljs.spec.alpha :as s] - [status-im.utils.config :as config] - [status-im.utils.random :as random])) - -;; user -(def send-message! chat/send!) -(def send-seen! chat/send-seen!) -(def reset-pending-messages! d/reset-pending-messages!) - -;; group -(def start-watching-group! group/start-watching-group!) -(def stop-watching-group! group/stop-watching-group!) -(def send-group-message! group/send!) -(def send-public-group-message! group/send-to-public-group!) -(def invite-to-group! group/invite!) -(def update-group! group/update-group!) -(def remove-from-group! group/remove-identity!) -(def add-to-group! group/add-identity!) -(def leave-group-chat! group/leave!) - -;; encryption -;; todo move somewhere, encryption functions shouldn't be there -(def new-keypair! e/new-keypair!) - -;; discoveries -(def watch-user! discoveries/watch-user!) -(def stop-watching-user! discoveries/stop-watching-user!) -(def contact-request! discoveries/contact-request!) -(def broadcast-profile! discoveries/broadcast-profile!) -(def send-status! discoveries/send-status!) -(def send-discoveries-request! discoveries/send-discoveries-request!) -(def send-discoveries-response! discoveries/send-discoveries-response!) -(def update-keys! discoveries/update-keys!) - -(def message-pending? d/message-pending?) - -;; initialization -(s/def ::identity string?) -(s/def :message/chat-id string?) -(s/def ::public? (s/and boolean? true?)) -(s/def ::group-id :message/chat-id) -(s/def ::group (s/or - :group (s/keys :req-un [::group-id :message/keypair]) - :public-group (s/keys :req-un [::group-id ::public?]))) -(s/def ::groups (s/* ::group)) -(s/def ::callback fn?) -(s/def ::contact (s/keys :req-un [::identity :message/keypair])) -(s/def ::contacts (s/* ::contact)) -(s/def ::profile-keypair :message/keypair) -(s/def ::options - (s/merge - (s/keys :req-un [::identity ::groups ::profile-keypair - ::callback :discoveries/hashtags ::contacts]) - ::d/delivery-options)) - -(def stop-watching-all! f/remove-all-filters!) -(def reset-all-pending-messages! d/reset-all-pending-messages!) -(def reset-keys! shh-keys/reset-keys!) - -(defn stop-whisper! [] - (stop-watching-all!) - (reset-all-pending-messages!) - (reset-keys!)) - -(defn init-whisper! - [{:keys [identity groups callback web3 - contacts profile-keypair pending-messages] - :as options}] - {:pre [(valid? ::options options)]} - (debug :init-whisper) - (stop-whisper!) - (let [listener-options {:web3 web3 - :identity identity - :callback callback}] - ;; start listening to groups - (doseq [group groups] - (let [options (merge listener-options group)] - (group/start-watching-group! options))) - ;; start listening to user's inbox - (if config/offline-inbox-enabled? - (do (log/info "offline inbox: flag enabled") - (f/add-filter! - web3 - {:key identity - :allowP2P true - :topics (f/get-topics identity)} - (l/message-listener listener-options)) - (inbox/initialize! web3)) - (f/add-filter! - web3 - {:key identity - :topics (f/get-topics identity)} - (l/message-listener listener-options))) - - ;; start listening to profiles - (doseq [{:keys [identity keypair]} contacts] - (watch-user! {:web3 web3 - :identity identity - :keypair keypair - :callback callback})) - (d/set-pending-mesage-callback! callback) - (let [online-message #(discoveries/send-online! - {:web3 web3 - :message {:from identity - :message-id (random/id) - :keypair profile-keypair}})] - (d/run-delivery-loop! - web3 - (assoc options :online-message online-message))) - (doseq [pending-message pending-messages] - (d/add-prepared-pending-message! web3 pending-message)))) diff --git a/src/status_im/protocol/discoveries.cljs b/src/status_im/protocol/discoveries.cljs deleted file mode 100644 index f3c8ec0173..0000000000 --- a/src/status_im/protocol/discoveries.cljs +++ /dev/null @@ -1,182 +0,0 @@ -(ns status-im.protocol.discoveries - (:require - [taoensso.timbre :refer-macros [debug]] - [status-im.protocol.web3.utils :as u] - [status-im.protocol.web3.delivery :as d] - [status-im.protocol.web3.filtering :as f] - [status-im.protocol.listeners :as l] - [cljs.spec.alpha :as s] - [status-im.protocol.validation :refer-macros [valid?]] - [status-im.utils.random :as random] - [status-im.protocol.web3.keys :as shh-keys] - [status-im.utils.datetime :as datetime])) - -(def discover-topic-prefix "status-discover-") -(def discover-topic "0xbeefdead") - -(defn- make-discover-topic [identity] - (str discover-topic-prefix identity)) - -(s/def :send-online/message - (s/merge :protocol/message - (s/keys :req-un [:message/keypair]))) -(s/def :send-online/options - (s/keys :req-un [:options/web3 :send-online/message])) - -(def discovery-key-password "status-discovery") - -(defn send-online! - [{:keys [web3 message] :as options}] - {:pre [(valid? :send-online/options options)]} - (debug :send-online) - (let [message' (merge - message - {:requires-ack? false - :type :online - :key-password discovery-key-password - :payload {:content {:timestamp (datetime/timestamp)}} - :topics [f/status-topic]})] - (d/add-pending-message! web3 message'))) - -(s/def ::identity :message/from) -(s/def :watch-user/options - (s/keys :req-un [:options/web3 :message/keypair ::identity ::callback])) - -(defn watch-user! - [{:keys [web3 identity] :as options}] - {:pre [(valid? :watch-user/options options)]} - (shh-keys/get-sym-key - web3 - discovery-key-password - (fn [key-id] - (f/add-filter! - web3 - {:sig identity - :topics [f/status-topic] - :key key-id - :type :sym} - (l/message-listener (dissoc options :identity)))))) - -(defn stop-watching-user! - [{:keys [web3 identity]}] - (shh-keys/get-sym-key - web3 - discovery-key-password - (fn [key-id] - (f/remove-filter! - web3 - {:sig identity - :topics [f/status-topic] - :key key-id - :type :sym})))) - -(s/def :contact-request/contact map?) - -(s/def :contact-request/payload - (s/merge :message/payload - (s/keys :req-un [:contact-request/contact :message/keypair]))) - -(s/def :contact-request/message - (s/merge :protocol/message - (s/keys :req-un [:message/to :contact-request/payload]))) - -(defn contact-request! - [{:keys [web3 message]}] - {:pre [(valid? :contact-request/message message)]} - (debug :send-command-request!) - (d/add-pending-message! - web3 - (assoc message - :type :contact-request - :requires-ack? true - :topics [f/status-topic]))) - -(s/def :discoveries/hashtags (s/every string? :kind-of set?)) - -(s/def ::callback fn?) -(s/def :watch-hashtags/options - (s/keys :req-un [:options/web3 :discoveries/hashtags ::callback])) - -(s/def ::status (s/nilable string?)) -(s/def ::profile (s/keys :req-un [::status])) -(s/def :profile/payload - (s/merge :message/payload (s/keys :req-un [::profile]))) -(s/def :profile/message - (s/merge :protocol/message (s/keys :req-un [:message/keypair - :profile/payload]))) -(s/def :broadcast-profile/options - (s/keys :req-un [:profile/message :options/web3])) - -(defn broadcast-profile! - [{:keys [web3 message] :as options}] - {:pre [(valid? :broadcast-profile/options options)]} - (debug :broadcasting-status) - (d/add-pending-message! - web3 - (-> message - (assoc :type :profile - :topics [f/status-topic] - :key-password discovery-key-password) - (assoc-in [:payload :timestamp] (datetime/timestamp)) - (assoc-in [:payload :content :profile] - (get-in message [:payload :profile])) - (update :payload dissoc :profile)))) - -(s/def ::public string?) -(s/def ::private string?) -(s/def ::keypair (s/keys :req-un [::public ::private])) -(s/def :update-keys/payload - (s/keys :req-un [::keypair])) -(s/def :update-keys/message - (s/merge :protocol/message (s/keys :req-un [:update-keys/payload]))) -(s/def :update-keys/options - (s/keys :req-un [:update-keys/message :options/web3])) - -(defn update-keys! - [{:keys [web3 message] :as options}] - {:pre [(valid? :update-keys/options options)]} - (let [message (-> message - (assoc :type :update-keys - :requires-ack? false - :key-password discovery-key-password - :topics [f/status-topic]) - (assoc-in [:payload :timestamp] (datetime/timestamp)))] - (d/add-pending-message! web3 message))) - -(s/def :status/payload - (s/merge :message/payload (s/keys :req-un [::status]))) -(s/def :status/message - (s/merge :protocol/message (s/keys :req-un [:status/payload]))) -(s/def :broadcast-hasthags/options - (s/keys :req-un [:discoveries/hashtags :status/message :options/web3])) - -(defn send-status! - [{:keys [web3 message]}] - (debug :broadcasting-status) - (let [message (assoc message :type :discover - :key-password discovery-key-password - :topics [f/status-topic])] - (d/add-pending-message! web3 message))) - -(defn send-discoveries-request! - [{:keys [web3 message]}] - (debug :sending-discoveries-request) - (d/add-pending-message! - web3 - (assoc message :type :discoveries-request - :key-password discovery-key-password - :topics [f/status-topic]))) - -(defn send-discoveries-response! - [{:keys [web3 discoveries message]}] - (debug :sending-discoveries-response) - (doseq [portion (->> discoveries - (take 100) - (partition 10 10 nil))] - (d/add-pending-message! - web3 - (assoc message :type :discoveries-response - :key-password discovery-key-password - :topics [f/status-topic] - :message-id (random/id) - :payload {:data (into [] portion)})))) diff --git a/src/status_im/protocol/encryption.cljs b/src/status_im/protocol/encryption.cljs deleted file mode 100644 index 5fb9fa23bd..0000000000 --- a/src/status_im/protocol/encryption.cljs +++ /dev/null @@ -1,21 +0,0 @@ -(ns status-im.protocol.encryption - (:require [status-im.js-dependencies :as dependencies])) - -(def default-curve 384) - -(defn new-keypair! - "Returns {:private \"private key\" :public \"public key\"" - [] - (let [{:keys [enc dec]} - (-> dependencies/eccjs - (.generate (.-ENC_DEC dependencies/eccjs) default-curve) - (js->clj :keywordize-keys true))] - {:private dec - :public enc})) - -(defn encrypt [public-key content] - (.encrypt dependencies/eccjs public-key content)) - -(defn decrypt [private-key content] - (.decrypt dependencies/eccjs private-key content)) - diff --git a/src/status_im/protocol/group.cljs b/src/status_im/protocol/group.cljs deleted file mode 100644 index 0ea56255e1..0000000000 --- a/src/status_im/protocol/group.cljs +++ /dev/null @@ -1,153 +0,0 @@ -(ns status-im.protocol.group - (:require - [status-im.protocol.web3.delivery :as d] - [status-im.protocol.web3.utils :as u] - [status-im.utils.config :as config] - [cljs.spec.alpha :as s] - [taoensso.timbre :refer-macros [debug]] - [status-im.protocol.validation :refer-macros [valid?]] - [status-im.protocol.web3.filtering :as f] - [status-im.protocol.listeners :as l] - [clojure.string :as str] - [status-im.protocol.web3.keys :as shh-keys] - [status-im.utils.datetime :as datetime])) - -(defn prepare-mesage - [{:keys [message group-id keypair new-keypair type username requires-ack?]}] - (let [message' (-> message - (update :payload assoc - :username username - :group-id group-id - :type type - :timestamp (datetime/timestamp)) - (assoc :topics [f/status-topic] - :key-password group-id - :requires-ack? (or (nil? requires-ack?) requires-ack?) - :type type))] - (cond-> message' - keypair (assoc :keypair keypair) - new-keypair (assoc :new-keypair keypair)))) - -(defn- send-group-message! - [{:keys [web3 group-id] :as opts} type] - (let [message (-> opts - (assoc :type type - :key-password group-id) - (prepare-mesage))] - (debug :send-group-message message) - (d/add-pending-message! web3 message))) - -(s/def ::message - (s/merge :protocol/message (s/keys :req-un [:chat-message/payload]))) - -(s/def :public-group/username (s/and string? (complement str/blank?))) -(s/def :public-group/message - (s/merge ::message (s/keys :username :public-group/username))) - -(defn send! - [{:keys [keypair message] :as options}] - {:pre [(valid? :message/keypair keypair) - (valid? ::message message)]} - (send-group-message! options :group-message)) - -(defn send-to-public-group! - [{:keys [message] :as options}] - {:pre [(valid? :public-group/message message)]} - (send-group-message! (assoc options :requires-ack? false) - :public-group-message)) - -(defn leave! - [options] - (send-group-message! options :leave-group)) - -(defn add-identity! - [{:keys [identity] :as options}] - {:pre [(valid? :message/to identity)]} - (let [options' (assoc-in options - [:message :payload :identity] - identity)] - (send-group-message! options' :add-group-identity))) - -(defn remove-identity! - [{:keys [identity] :as options}] - {:pre [(valid? :message/to identity)]} - (let [options' (assoc-in options - [:message :payload :identity] - identity)] - (send-group-message! options' :remove-group-identity))) - -(s/def ::identities (s/* string?)) - -(s/def ::name string?) -(s/def ::id string?) -(s/def ::admin string?) -(s/def ::contacts (s/* string?)) -(s/def ::group - (s/keys :req-un - [::name ::id ::contacts :message/keypair ::admin])) -(s/def :invite/options - (s/keys :req-un [:options/web3 :protocol/message ::group ::identities])) - -(defn- notify-about-group! - [type {:keys [web3 message identities group] - :as options}] - {:pre [(valid? :invite/options options)]} - (let [{:keys [id admin name keypair contacts]} group - message' (-> message - (assoc :topics [f/status-topic] - :requires-ack? true - :type type) - (update :payload assoc - :timestamp (datetime/timestamp) - :group-id id - :group-admin admin - :group-name name - :keypair keypair - :contacts contacts - :type type))] - (doseq [identity identities] - (d/add-pending-message! web3 (assoc message' :to identity))))) - -(defn invite! - [options] - (notify-about-group! :group-invitation options)) - -;; todo notify users about keypair change when someone leaves group (from admin) -(defn update-group! - [options] - (notify-about-group! :update-group options)) - -(defn stop-watching-group! - [{:keys [web3 group-id]}] - {:pre [(valid? :message/chat-id group-id)]} - (shh-keys/get-sym-key - web3 - group-id - (fn [key-id] - (f/remove-filter! - web3 - {:topics [f/status-topic] - :key key-id - :type :sym})))) - -(defn start-watching-group! - [{:keys [web3 group-id keypair callback identity]}] - (shh-keys/get-sym-key - web3 - group-id - (fn [key-id] - (f/add-filter! - web3 - (if (and config/offline-inbox-enabled? - config/offline-inbox-many-enabled?) - {:topics [f/status-topic] - :key key-id - :allowP2P true - :type :sym} - {:topics [f/status-topic] - :key key-id - :type :sym}) - (l/message-listener {:web3 web3 - :identity identity - :callback callback - :keypair keypair}))))) diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index 296340d866..86156e1c61 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -1,93 +1,21 @@ (ns status-im.protocol.handlers - (:require [re-frame.core :as re-frame] - [cljs.core.async :as async] - [status-im.utils.handlers :as handlers] - [status-im.data-store.contacts :as contacts] - [status-im.data-store.messages :as messages] - [status-im.data-store.pending-messages :as pending-messages] - [status-im.data-store.processed-messages :as processed-messages] - [status-im.data-store.chats :as chats] - [status-im.protocol.core :as protocol] + (:require [cljs.core.async :as async] + [re-frame.core :as re-frame] [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.utils.random :as random] - [status-im.utils.async :as async-utils] - [status-im.protocol.message-cache :as cache] - [status-im.protocol.listeners :as listeners] - [status-im.chat.models.message :as models.message] - [status-im.chat.models :as chat] - [status-im.protocol.web3.inbox :as inbox] - [status-im.protocol.web3.keys :as web3.keys] - [status-im.utils.datetime :as datetime] - [taoensso.timbre :as log] [status-im.native-module.core :as status] - [clojure.string :as string] - [status-im.utils.web3-provider :as web3-provider] + [status-im.transport.message-cache :as message-cache] + [status-im.utils.datetime :as datetime] [status-im.utils.ethereum.core :as utils] - [status-im.utils.config :as config])) + [status-im.utils.handlers :as handlers] + [status-im.utils.web3-provider :as web3-provider] + [status-im.transport.core :as transport])) ;;;; COFX - (re-frame/reg-cofx ::get-web3 (fn [coeffects _] (assoc coeffects :web3 (web3-provider/make-web3)))) -(re-frame/reg-cofx - ::get-chat-groups - (fn [coeffects _] - (assoc coeffects :groups (chats/get-active-group-chats)))) - -(re-frame/reg-cofx - ::get-pending-messages - (fn [coeffects _] - (assoc coeffects :pending-messages (pending-messages/get-all)))) - -(re-frame/reg-cofx - ::message-get-by-id - (fn [coeffects _] - (let [[{{:keys [message-id]} :payload}] (:event coeffects)] - (assoc coeffects :message-by-id (messages/get-by-id message-id))))) - - -;;;; FX - -(def ^:private protocol-realm-queue (async-utils/task-queue 2000)) - -(re-frame/reg-fx - :stop-whisper - (fn [] (protocol/stop-whisper!))) - -(re-frame/reg-fx - ::init-whisper - (fn [{:keys [web3 public-key groups updates-public-key updates-private-key status contacts pending-messages]}] - (protocol/init-whisper! - {:web3 web3 - :identity public-key - :groups groups - :callback #(re-frame/dispatch [:incoming-message %1 %2]) - :ack-not-received-s-interval 125 - :default-ttl 120 - :send-online-s-interval 180 - :ttl-config {:public-group-message 2400} - :max-attempts-number 3 - :delivery-loop-ms-interval 500 - :profile-keypair {:public updates-public-key - :private updates-private-key} - :hashtags (mapv name (handlers/get-hashtags status)) - :pending-messages pending-messages - :contacts (keep (fn [{:keys [whisper-identity - public-key - private-key]}] - (when (and public-key private-key) - {:identity whisper-identity - :keypair {:public public-key - :private private-key}})) - contacts) - :post-error-callback #(re-frame/dispatch [::post-error %]) - :pow-target config/pow-target - :pow-time config/pow-time}))) - (re-frame/reg-fx ::web3-get-syncing (fn [web3] @@ -97,314 +25,24 @@ (fn [error sync] (re-frame/dispatch [:update-sync-state error sync])))))) -(re-frame/reg-fx - ::save-processed-messages - (fn [processed-message] - (async/go (async/>! protocol-realm-queue #(processed-messages/save processed-message))))) - -(defn system-message [message-id timestamp content] - {:from "system" - :message-id message-id - :timestamp timestamp - :content content - :content-type constants/text-content-type}) - -(re-frame/reg-fx - ::participant-removed-from-group-message - (fn [{:keys [identity from message-id timestamp group-id contacts]}] - (let [remover-name (get-in contacts [from :name]) - removed-name (get-in contacts [identity :name]) - message (->> [(or remover-name from) (i18n/label :t/removed) (or removed-name identity)] - (string/join " ") - (system-message message-id timestamp)) - message' (assoc message :group-id group-id)] - (re-frame/dispatch [:chat-received-message/add message'])))) - -(re-frame/reg-fx - ::chats-add-contact - (fn [[group-id identity]] - (chats/add-contacts group-id [identity]))) - -(re-frame/reg-fx - ::chats-remove-contact - (fn [[group-id identity]] - (chats/remove-contacts group-id [identity]))) - -(re-frame/reg-fx - ::you-removed-from-group-message - (fn [{:keys [from message-id timestamp group-id contacts]}] - (let [remover-name (get-in contacts [from :name]) - message (->> [(or remover-name from) (i18n/label :t/removed-from-chat)] - (string/join " ") - (system-message message-id timestamp)) - message' (assoc message :group-id group-id)] - (re-frame/dispatch [:chat-received-message/add message'])))) - -(re-frame/reg-fx - ::stop-watching-group! - (fn [params] - (protocol/stop-watching-group! params))) - -(re-frame/reg-fx - ::participant-left-group-message - (fn [{:keys [chat-id from message-id timestamp contacts]}] - (let [left-name (get-in contacts [from :name]) - message-text (str (or left-name from) " " (i18n/label :t/left))] - (-> (system-message message-id timestamp message-text) - (assoc :chat-id chat-id) - (messages/save))))) - -(re-frame/reg-fx - ::participant-invited-to-group-message - (fn [{:keys [group-id current-identity identity from message-id timestamp contacts]}] - (let [inviter-name (get-in contacts [from :name]) - invitee-name (if (= identity current-identity) - (i18n/label :t/You) - (get-in contacts [identity :name]))] - (re-frame/dispatch - [:chat-received-message/add - {:from "system" - :group-id group-id - :timestamp timestamp - :message-id message-id - :content (str (or inviter-name from) " " (i18n/label :t/invited) " " (or invitee-name identity)) - :content-type constants/text-content-type}])))) - -(re-frame/reg-fx - ::pending-messages-delete - (fn [message-id] - (async/go (async/>! protocol-realm-queue #(pending-messages/delete message-id))))) - -(re-frame/reg-fx - ::pending-messages-save - (fn [pending-message] - (async/go (async/>! protocol-realm-queue #(pending-messages/save pending-message))))) - (re-frame/reg-fx ::status-init-jail (fn [] (status/init-jail))) -(re-frame/reg-fx - ::load-processed-messages! - (fn [] - (let [now (datetime/timestamp) - messages (processed-messages/get-filtered (str "ttl > " now))] - (cache/init! messages) - (processed-messages/delete (str "ttl <=" now))))) - -(re-frame/reg-fx - ::add-peer - (fn [{:keys [wnode web3]}] - (inbox/add-peer wnode - #(re-frame/dispatch [::add-peer-success web3 %]) - #(re-frame/dispatch [::add-peer-error %])))) - -(re-frame/reg-fx - ::fetch-peers - (fn [{:keys [wnode web3 retries]}] - ;; Run immediately on first run, add delay before retry - (let [delay (cond - (zero? retries) 0 - (< retries 3) 300 - (< retries 10) 1000 - :else 5000)] - (if (> retries 100) - (log/error "Number of retries for fetching peers exceed" wnode) - (js/setTimeout - (fn [] (inbox/fetch-peers #(re-frame/dispatch [::fetch-peers-success web3 % retries]) - #(re-frame/dispatch [::fetch-peers-error %]))) - delay))))) - -(re-frame/reg-fx - ::mark-trusted-peer - (fn [{:keys [wnode web3 peers]}] - (inbox/mark-trusted-peer web3 - wnode - peers - #(re-frame/dispatch [::mark-trusted-peer-success web3 %]) - #(re-frame/dispatch [::mark-trusted-peer-error %])))) - -(re-frame/reg-fx - ::get-sym-key - (fn [{:keys [web3 password]}] - (web3.keys/get-sym-key web3 - password - #(re-frame/dispatch [::get-sym-key-success web3 %]) - #(re-frame/dispatch [::get-sym-key-error %])))) - -(re-frame/reg-fx - ::request-messages - (fn [{:keys [wnode topic sym-key-id web3]}] - (inbox/request-messages web3 - wnode - topic - sym-key-id - #(re-frame/dispatch [::request-messages-success %]) - #(re-frame/dispatch [::request-messages-error %])))) - -(re-frame/reg-fx - ::handle-whisper-message - listeners/handle-whisper-message) - -;;;; Handlers - - -(defn get-wnode [db] - (let [wnode-id (get db :inbox/wnode)] - (get-in db [:inbox/wnodes wnode-id :address]))) - -(defn connectivity-check [peers {:keys [db] :as cofx}] - (let [wnode (get-wnode db) - peers-count (count peers) - mailserver-connected? (inbox/registered-peer? peers wnode)] - {:db (cond-> db - mailserver-connected? (dissoc :mailserver-status) - (not mailserver-connected?) (assoc :mailserver-status :disconnected) - :always (assoc :peers-count peers-count))})) - -(re-frame/reg-fx - :connectivity/fetch-peers - (fn [{:keys [wnode web3 retries]}] - (inbox/fetch-peers #(re-frame/dispatch [:connectivity-check-success %]) - #(log/error :connectivity/fetch-peers %)))) - -(handlers/register-handler-fx - :connectivity-check - (fn [{:keys [db]} _] - (let [web3 (:web3 db) - wnode (get-wnode db)] - {:connectivity/fetch-peers {:wnode wnode - :web3 web3}}))) - -(handlers/register-handler-fx - :connectivity-check-success - (fn [{:keys [db] :as cofx} [_ peers]] - (handlers/merge-fx cofx - {:dispatch-later [{:ms 30000 :dispatch [:connectivity-check]}]} - (connectivity-check peers)))) - -;; NOTE(dmitryn): events chain -;; add-peer -> fetch-peers -> mark-trusted-peer -> get-sym-key -> request-messages -(handlers/register-handler-fx - :initialize-offline-inbox - (fn [{:keys [db]} [_ web3]] - (log/info "offline inbox: initialize") - (let [wnode (get-wnode db)] - {::add-peer {:wnode wnode - :web3 web3}}))) - -(handlers/register-handler-fx - ::add-peer-success - (fn [{:keys [db]} [_ web3 response]] - (let [wnode (get-wnode db)] - (log/info "offline inbox: add-peer response" wnode response) - {::fetch-peers {:wnode wnode - :web3 web3 - :retries 0}}))) - -(handlers/register-handler-fx - ::fetch-peers-success - (fn [{:keys [db] :as cofx} [_ web3 peers retries]] - (let [wnode (get-wnode db)] - (log/info "offline inbox: fetch-peers response" peers) - (if (inbox/registered-peer? peers wnode) - (handlers/merge-fx cofx - {::mark-trusted-peer {:wnode wnode - :web3 web3 - :peers peers} - :dispatch-later [{:ms 30000 :dispatch [:connectivity-check]}]} - (connectivity-check peers)) - (do - (log/info "Peer" wnode "is not registered. Retrying fetch peers.") - (handlers/merge-fx cofx - {::fetch-peers {:wnode wnode - :web3 web3 - :retries (inc retries)}} - (connectivity-check peers))))))) - -(handlers/register-handler-fx - ::mark-trusted-peer-success - (fn [{:keys [db]} [_ web3 response]] - (let [wnode (get-wnode db) - password (:inbox/password db)] - (log/info "offline inbox: mark-trusted-peer response" wnode response) - {::get-sym-key {:password password - :web3 web3}}))) - - - -(handlers/register-handler-fx - ::get-sym-key-success - (fn [{:keys [db]} [_ web3 sym-key-id]] - (log/info "offline inbox: get-sym-key response" sym-key-id) - (let [wnode (get-wnode db) - topic (:inbox/topic db)] - {::request-messages {:wnode wnode - :topic topic - :sym-key-id sym-key-id - :web3 web3}}))) - -(handlers/register-handler-fx - ::request-messages-success - (fn [_ [_ response]] - (log/info "offline inbox: request-messages response" response))) - -(handlers/register-handler-fx - ::add-peer-error - (fn [_ [_ error]] - (log/error "offline inbox: add-peer error" error))) - -(handlers/register-handler-fx - ::fetch-peers-error - (fn [_ [_ error]] - (log/error "offline inbox: fetch-peers error" error))) - -(handlers/register-handler-fx - ::mark-trusted-peer-error - (fn [_ [_ error]] - (log/error "offline inbox: mark-trusted-peer error" error))) - -(handlers/register-handler-fx - ::get-sym-key-error - (fn [_ [_ error]] - (log/error "offline inbox: get-sym-key error" error))) - -(handlers/register-handler-fx - ::request-messages-error - (fn [_ [_ error]] - (log/error "offline inbox: request-messages error" error))) - -(handlers/register-handler-fx - :handle-whisper-message - (fn [_ [_ error msg options]] - {::handle-whisper-message {:error error - :msg msg - :options options}})) - ;;; INITIALIZE PROTOCOL (handlers/register-handler-fx :initialize-protocol [re-frame/trim-v (re-frame/inject-cofx ::get-web3) - (re-frame/inject-cofx ::get-chat-groups) - (re-frame/inject-cofx ::get-pending-messages) - (re-frame/inject-cofx :get-all-contacts)] - (fn [{:keys [db web3 groups all-contacts pending-messages]} [current-account-id ethereum-rpc-url]] - (let [{:keys [public-key status updates-public-key - updates-private-key]} - (get-in db [:accounts/accounts current-account-id])] - (when public-key - {::init-whisper {:web3 web3 :public-key public-key :groups groups :pending-messages pending-messages - :updates-public-key updates-public-key :updates-private-key updates-private-key - :status status :contacts all-contacts} - :db (assoc db :web3 web3 - :rpc-url (or ethereum-rpc-url constants/ethereum-rpc-url))})))) - -(handlers/register-handler-fx - :load-processed-messages - (fn [_ _] - {::load-processed-messages! nil})) + (re-frame/inject-cofx :data-store/transport)] + (fn [{:data-store/keys [transport] :keys [db web3] :as cofx} [current-account-id ethereum-rpc-url]] + (handlers/merge-fx cofx + {:db (assoc db + :web3 web3 + :rpc-url (or ethereum-rpc-url constants/ethereum-rpc-url) + :transport/chats transport)} + (transport/init-whisper current-account-id)))) ;;; NODE SYNC STATE @@ -433,216 +71,12 @@ :check-sync (fn [{{:keys [web3]} :db} _] {::web3-get-syncing web3 - :dispatch-later [{:ms 10000 :dispatch [:check-sync]}]})) + :dispatch-later [{:ms 10000 :dispatch [:check-sync]}]})) (handlers/register-handler-fx :initialize-sync-listener (fn [{{:keys [sync-listening-started network networks/networks] :as db} :db} _] (when (and (not sync-listening-started) (not (utils/network-with-upstream-rpc? networks network))) - {:db (assoc db :sync-listening-started true) + {:db (assoc db :sync-listening-started true) :dispatch [:check-sync]}))) - -;;; MESSAGES - -(defn- transform-protocol-message [{:keys [from to payload]}] - (merge payload {:from from - :to to - :chat-id (or (:group-id payload) from)})) - -(defn- message-from-self [{:keys [current-public-key]} {:keys [id to group-id]}] - {:from to - :sent-from current-public-key - :payload {:message-id id - :group-id group-id}}) - -(defn- get-message-id [{:keys [message-id ack-of-message]}] - (or ack-of-message message-id)) - -(handlers/register-handler-fx - :incoming-message - (fn [{:keys [db]} [_ type {:keys [payload ttl id] :as message}]] - (let [message-id (or id (:message-id payload))] - (when-not (cache/exists? message-id type) - (let [ttl-s (* 1000 (or ttl 120)) - processed-message {:id (random/id) - :message-id message-id - :type type - :ttl (+ (datetime/timestamp) ttl-s)} - chat-message (#{:message :group-message} (:type payload)) - route-fx (case type - (:message - :group-message - :public-group-message) {:dispatch [:pre-received-message (transform-protocol-message message)]} - :pending (cond-> {::pending-messages-save message} - chat-message - (assoc :dispatch - [:update-message-status (message-from-self db message) :pending])) - :sent {:dispatch [:update-message-status (message-from-self db message) :sent]} - :ack (cond-> {::pending-messages-delete (get-message-id payload)} - chat-message - (assoc :dispatch [:update-message-status message :delivered])) - :seen {:dispatch [:update-message-status message :seen]} - :group-invitation {:dispatch [:group-chat-invite-received message]} - :update-group {:dispatch [:update-group-message message]} - :add-group-identity {:dispatch [:participant-invited-to-group message]} - :remove-group-identity {:dispatch [:participant-removed-from-group message]} - :leave-group {:dispatch [:participant-left-group message]} - :contact-request {:dispatch [:contact-request-received message]} - :discover {:dispatch [:status-received message]} - :discoveries-request {:dispatch [:discoveries-request-received message]} - :discoveries-response {:dispatch [:discoveries-response-received message]} - :profile {:dispatch [:contact-update-received message]} - :update-keys {:dispatch [:update-keys-received message]} - :online {:dispatch [:contact-online-received message]} - nil)] - (when (nil? route-fx) (log/debug "Unknown message type" type)) - (cache/add! processed-message) - (merge - {::save-processed-messages processed-message} - route-fx)))))) - -(handlers/register-handler-fx - :update-message-status - [re-frame/trim-v (re-frame/inject-cofx :get-stored-message)] - (fn [{:keys [db get-stored-message]} [{:keys [from sent-from payload]} status]] - (let [message-identifier (get-message-id payload) - chat-identifier (or (:group-id payload) from) - message-db-path [:chats chat-identifier :messages message-identifier] - from-id (or sent-from from) - message (or (get-in db message-db-path) - (and (get (:not-loaded-message-ids db) message-identifier) - (get-stored-message message-identifier)))] - ;; proceed with updating status if chat is in db, status is not the same and message was not already seen - (when (and message - (get-in db [:chats chat-identifier]) - (not= status (get-in message [:user-statuses from-id])) - (not (models.message/message-seen-by? message from-id))) - (let [statuses (assoc (:user-statuses message) from-id status)] - (cond-> {:update-message {:message-id message-identifier - :user-statuses statuses}} - (get-in db message-db-path) - (assoc :db (assoc-in db (conj message-db-path :user-statuses) statuses)))))))) - -(handlers/register-handler-fx - :contact-request-received - (fn [{{:contacts/keys [contacts]} :db} - [_ {:keys [from payload timestamp]}]] - (when from - (let [{{:keys [name profile-image address status fcm-token]} :contact - {:keys [public private]} :keypair} payload - existing-contact (get contacts from) - contact {:whisper-identity from - :public-key public - :private-key private - :address address - :status status - :photo-path profile-image - :name name - :fcm-token fcm-token} - chat {:name name - :chat-id from - :contact-info (prn-str contact)} - prev-last-updated (get-in contacts [from :last-updated] 0) - ;; NOTE(dmitryn) Workaround for old messages not having "payload.timestamp" attribute. - ;; Get timestamp from message root level. - ;; Root level "timestamp" is a unix ts in seconds. - timestamp' (or (:payload timestamp) - (* 1000 timestamp))] - (if-not existing-contact - (let [contact (assoc contact :pending? true)] - {:dispatch-n [[:add-contacts [contact]] - [:add-chat from chat]]}) - (when-not (:pending? existing-contact) - (cond-> {:dispatch-n [[:update-chat! chat] - [:watch-contact contact]]} - (<= prev-last-updated timestamp') (update :dispatch-n concat [[:update-contact! contact]])))))))) - -;;GROUP - -(defn- has-contact? [{:keys [contacts]} identity] - (let [identities (set (map :identity contacts))] - (contains? identities identity))) - -(handlers/register-handler-fx - :participant-invited-to-group - [re-frame/trim-v] - (fn [{{:keys [current-public-key chats contacts/contacts] :as db} :db} - [{:keys [from] - {:keys [group-id identity message-id timestamp]} :payload}]] - (let [chat (get-in db [:chats group-id]) - admin (:group-admin chats)] - (when (= from admin) - (merge {::participant-invited-to-group-message {:group-id group-id :current-public-key current-public-key - :identity identity :from from :message-id message-id - :timestamp timestamp :contacts contacts}} - (when-not (and (= current-public-key identity) (has-contact? chat identity)) - {:db (update-in db [:chats group-id :contacts] conj {:identity identity}) - ::chats-add-contact [group-id identity]})))))) - -(handlers/register-handler-fx - ::you-removed-from-group - [re-frame/trim-v] - (fn [{{:keys [web3 contacts/contacts] :as db} :db} - [{:keys [from] - {:keys [group-id timestamp message-id]} :payload}]] - (let [chat (get-in db [:chats group-id]) - new-update? (chat/new-update? chat timestamp)] - (when new-update? - {::you-removed-from-group-message {:from from - :message-id message-id - :timestamp timestamp - :group-id group-id - :contacts contacts} - ::stop-watching-group! {:web3 web3 - :group-id group-id} - :dispatch [:update-chat! {:chat-id group-id - :removed-from-at timestamp - :is-active false}]})))) - -(handlers/register-handler-fx - :participant-removed-from-group - [re-frame/trim-v - (re-frame/inject-cofx ::message-get-by-id)] - (fn [{{:keys [current-public-key chats contacts/contacts]} :db message-by-id :message-by-id} - [{:keys [from] - {:keys [group-id identity message-id timestamp]} :payload - :as message}]] - (when-not message-by-id - (let [admin (get-in chats [group-id :group-admin])] - (when (= admin from) - (if (= current-public-key identity) - {:dispatch [::you-removed-from-group message]} - {::participant-removed-from-group-message {:identity identity :from from :message-id message-id - :timestamp timestamp :group-id group-id :contacts contacts} - ::chats-remove-contact [group-id identity]})))))) - -(handlers/register-handler-fx - :participant-left-group - [re-frame/trim-v] - (fn [{{:keys [current-public-key contacts/contacts] :as db} :db} - [{:keys [from] - {:keys [group-id timestamp message-id]} :payload}]] - (let [chat (get-in db [:chats group-id]) - {chat-is-active :is-active chat-timestamp :timestamp} chat] - (when (and (not= current-public-key from) - chat-is-active - (> timestamp chat-timestamp)) - {::participant-left-group-message {:chat-id group-id - :from from - :message-id message-id - :timestamp timestamp - :contacts contacts} - ::chats-remove-contact [group-id from] - :db (update-in db [:chats group-id :contacts] - #(remove (fn [{:keys [identity]}] - (= identity from)) %))})))) - -;;ERROR - -(handlers/register-handler-fx - ::post-error - (fn [_ [_ error]] - (let [android-error? (re-find (re-pattern "Failed to connect") (.-message error))] - (when android-error? - {::status-init-jail nil})))) diff --git a/src/status_im/protocol/listeners.cljs b/src/status_im/protocol/listeners.cljs deleted file mode 100644 index 4d29c61c09..0000000000 --- a/src/status_im/protocol/listeners.cljs +++ /dev/null @@ -1,100 +0,0 @@ -(ns status-im.protocol.listeners - (:require [cljs.reader :as r] - [re-frame.core :as re-frame] - [status-im.protocol.ack :as ack] - [status-im.protocol.web3.utils :as u] - [status-im.protocol.encryption :as e] - [taoensso.timbre :as log] - [status-im.utils.hex :as i])) - -(defn empty-public-key? [public-key] - (or (= "0x0" public-key) - (= "" public-key) - (nil? public-key))) - -(defn create-error [step description] - (when (not= description :silent) - (log/debug step description)) - {:error description}) - -(defn init-scope [js-error js-message options] - (if js-error - (create-error :init-scope-error (-> js-error js->clj str)) - {:message (js->clj js-message :keywordize-keys true) - :options options})) - -(defn parse-payload [{:keys [message error options] :as scope}] - (log/debug :parse-payload) - (if error - scope - (try - ;; todo figure why we sometimes have to call to-utf8 twice and sometimes only once - (let [payload (:payload message) - payload' (u/to-utf8 payload) - payload'' (r/read-string payload') - payload''' (if (map? payload'') - payload'' - (r/read-string (u/to-utf8 payload')))] - (if (map? payload''') - {:message (assoc message :payload payload''') - :options options} - (create-error :parse-payload-error (str "Invalid payload type " (type payload'''))))) - (catch :default err - (create-error :parse-payload-error err))))) - -(defn filter-messages-from-same-user [{:keys [message error options] :as scope}] - (if error - scope - (if (or (not= (i/normalize-hex (:identity options)) - (i/normalize-hex (:sig message))) - ;; allow user to receive his own discoveries - (= type :discover)) - scope - (create-error :filter-messages-error :silent)))) - -(defn parse-content [{:keys [message error options] :as scope}] - (if error - scope - (try - (let [to (:recipientPublicKey message) - from (:sig message) - key (get-in options [:keypair :private]) - raw-content (get-in message [:payload :content]) - encrypted? (and (empty-public-key? to) key raw-content) - content (if encrypted? - (r/read-string (e/decrypt key raw-content)) - raw-content)] - (log/debug :parse-content - "Key exists:" (not (nil? key)) - "Content exists:" (not (nil? raw-content))) - {:message (-> message - (assoc-in [:payload :content] content) - (assoc :to to - :from from)) - :options options}) - (catch :default err - (create-error :parse-content-error err))))) - -(defn handle-message [{:keys [message error options] :as scope}] - (if error - scope - (let [{:keys [web3 identity callback]} options - {:keys [payload sig]} message - ack? (get-in message [:payload :ack?])] - (log/debug :handle-message message) - (callback (if ack? :ack (:type payload)) message) - (ack/check-ack! web3 sig payload identity)))) - -(defn- handle-whisper-message [{:keys [error msg options]}] - (-> (init-scope error msg options) - parse-payload - filter-messages-from-same-user - parse-content - handle-message)) - -(defn message-listener - "Valid options are: web3, identity, callback, keypair" - [options] - (fn [js-error js-message] - (re-frame/dispatch [:handle-whisper-message js-error js-message options]))) - diff --git a/src/status_im/protocol/message.cljs b/src/status_im/protocol/message.cljs deleted file mode 100644 index 9104d3cf0a..0000000000 --- a/src/status_im/protocol/message.cljs +++ /dev/null @@ -1,52 +0,0 @@ -(ns status-im.protocol.message - (:require [cljs.spec.alpha :as s])) - -(s/def :message/ttl (s/and int? pos?)) -(s/def :message/from string?) -(s/def :message/sig :message/from) -(s/def :message/privateKeyID (s/nilable string?)) -(s/def :message/pub-key (s/nilable string?)) -(s/def :message/sym-key-password (s/nilable string?)) -(s/def :message/topic string?) -(s/def :message/to (s/nilable string?)) -(s/def :message/message-id string?) -(s/def :message/requires-ack? boolean?) -(s/def :keypair/private string?) -(s/def :keypair/public string?) -(s/def :message/keypair (s/keys :req-un [:keypair/private - :keypair/public])) -(s/def :message/topics (s/* string?)) - -(s/def :payload/content (s/or :string-message string? - :command map?)) -(s/def :payload/content-type string?) -(s/def :payload/timestamp (s/and int? pos?)) -(s/def :payload/new-keypair :message/keypair) - -(s/def :group-message/type - #{:public-group-message :group-message :group-invitation :add-group-identity - :remove-group-identity :leave-group :update-group}) - -(s/def :discover-message/type #{:online :status :discover :contact-request :update-keys}) - -(s/def :message/type - (s/or :group :group-message/type - :discover :discover-message/type - :user #{:message})) - -(s/def :message/payload - (s/keys :opt-un [:message/type - :payload/content - :payload/content-type - :payload/new-keypair - :payload/timestamp])) - -(s/def :protocol/message - (s/keys :req-un [:message/from :message/message-id] - :opt-un [:message/to :message/topics :message/requires-ack? - :message/keypair :message/ttl :message/payload])) - -(s/def :chat-message/payload - (s/keys :req-un [:payload/content :payload/content-type :payload/timestamp])) - -(s/def :options/web3 #(not (nil? %))) diff --git a/src/status_im/protocol/message_cache.cljs b/src/status_im/protocol/message_cache.cljs deleted file mode 100644 index db9a4fbdc9..0000000000 --- a/src/status_im/protocol/message_cache.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns status-im.protocol.message-cache - (:refer-clojure :exclude [exists?])) - -(defonce messages-set (atom #{})) -(defonce messages-map (atom {})) - -(defn init! - [messages] - (reset! messages-set (set messages)) - (reset! messages-map (->> messages - (map (fn [{:keys [message-id type] :as message}] - [[message-id type] message])) - (into {})))) - -(defn add! - [{:keys [message-id type] :as message}] - (swap! messages-set conj message) - (swap! messages-map conj [[message-id type] message])) - -(defn exists? - [message-id type] - (get @messages-map [message-id type])) diff --git a/src/status_im/protocol/validation.clj b/src/status_im/protocol/validation.clj deleted file mode 100644 index 1e6361b53e..0000000000 --- a/src/status_im/protocol/validation.clj +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.protocol.validation) - -(defn- fline [and-form] (:line (meta and-form))) - -(defmacro valid? [spec x] - `(let [v?# (cljs.spec.alpha/valid? ~spec ~x)] - (when-not v?# - (let [explanation# (cljs.spec.alpha/explain-str ~spec ~x)] - (taoensso.timbre/log! :error :p - [explanation#] - ~{:?line (fline &form)}))) - v?#)) diff --git a/src/status_im/protocol/validation.cljs b/src/status_im/protocol/validation.cljs deleted file mode 100644 index 44ba173656..0000000000 --- a/src/status_im/protocol/validation.cljs +++ /dev/null @@ -1,2 +0,0 @@ -(ns status-im.protocol.validation - (:require-macros [status-im.protocol.validation :as macros])) diff --git a/src/status_im/protocol/web3/delivery.cljs b/src/status_im/protocol/web3/delivery.cljs deleted file mode 100644 index a92fec7aa0..0000000000 --- a/src/status_im/protocol/web3/delivery.cljs +++ /dev/null @@ -1,271 +0,0 @@ -(ns status-im.protocol.web3.delivery - (:require [cljs.core.async :as async] - [status-im.protocol.web3.transport :as t] - [status-im.protocol.web3.utils :as u] - [status-im.protocol.encryption :as e] - [cljs.spec.alpha :as s] - [taoensso.timbre :refer-macros [debug] :as log] - [status-im.protocol.validation :refer-macros [valid?]] - [clojure.set :as set] - [status-im.protocol.web3.keys :as shh-keys] - [status-im.utils.async :refer [timeout]] - [status-im.utils.datetime :as datetime])) - -(defonce loop-state (atom nil)) -(defonce messages (atom {})) - -(defn prepare-message - [web3 {:keys [payload keypair to from topics ttl key-password] - :as message} - callback] - (let [{:keys [public]} keypair - - content (:content payload) - content' (if (and (not to) public content) - (e/encrypt public (prn-str content)) - content) - - payload' (-> message - (select-keys [:message-id :requires-ack? :type :clock-value]) - (merge payload) - (assoc :content content') - prn-str - u/from-utf8) - sym-key-password (or key-password shh-keys/status-key-password)] - (shh-keys/get-sym-key - web3 - sym-key-password - (fn [status-key-id] - (callback - (merge - (select-keys message [:ttl]) - (let [type (if to :asym :sym)] - (cond-> {:sig from - :topic (first topics) - :payload payload'} - to (assoc :pubKey to) - (not to) (assoc :symKeyID status-key-id - :sym-key-password sym-key-password))))))))) - -(s/def :shh/pending-message - (s/keys :req-un [:message/sig :shh/payload :message/topic] - :opt-un [:message/ttl :message/pubKey :message/symKeyID])) - -(defonce pending-message-callback (atom nil)) -(defonce recipient->pending-message (atom {})) - -;; Buffer needs to be big enough to not block even with many outbound messages -(def ^:private pending-message-queue (async/chan 2000)) - -(async/go-loop [[web3 {:keys [type message-id requires-ack? to ack?] :as message}] - (async/pending-message - update to set/union #{[web3 message-id to]})))))) - (recur (async/! pending-message-queue [web3 message]))) - -(s/def :delivery/pending-message - (s/keys :req-un [:message/sig :message/to :shh/payload :payload/ack? ::id - :message/requires-ack? :message/topic ::attempts ::was-sent?] - :opt-un [:message/pub-key :message/sym-key-password])) - -(defn- do-add-pending-message! - [web3 {:keys [message-id to pub-key sym-key-id] :as pending-message}] - (let [message (select-keys pending-message [:sig :topic :payload]) - message' (if sym-key-id - (assoc message :symKeyId sym-key-id) - (assoc message :pubKey pub-key)) - pending-message' (assoc pending-message :message message' - :id message-id)] - (swap! messages assoc-in [web3 message-id to] pending-message') - (when to - (swap! recipient->pending-message - update to set/union #{[web3 message-id to]})))) - -(defn add-prepared-pending-message! - [web3 {:keys [sym-key-password] :as pending-message}] - {:pre [(valid? :delivery/pending-message pending-message)]} - (debug :add-prepared-pending-message!) - (if sym-key-password - (shh-keys/get-sym-key - web3 - sym-key-password - (fn [sym-key-id] - (do-add-pending-message! web3 (assoc pending-message :sym-key-id sym-key-id)))) - (do-add-pending-message! web3 pending-message))) - -(defn remove-pending-message! [web3 id to] - (swap! messages update web3 - (fn [messages] - (when messages - (let [message (messages id) - ;; Message that is send without specified "from" option - ;; is stored in pending "messages" map as - ;; {message-id {nil message}}. - ;; When we receive the first ack for such message it is - ;; removed from pending messages adding of the nil key - ;; to the next dissoc form - ;; todo rewrite handling of ack message in more clear way - message' (dissoc message to nil)] - (if (seq message') - (assoc messages id message') - (dissoc messages id)))))) - (when to - (swap! recipient->pending-message - update to set/difference #{[web3 id to]}))) - -(defn message-was-sent! [web3 id to] - (let [messages' (swap! messages update web3 - (fn [messages] - (let [message (get-in messages [id to]) - message' (when message - (assoc message :was-sent? true - :attempts 1))] - (if message' - (assoc-in messages [id to] message') - messages))))] - (when @pending-message-callback - (let [message (get-in messages' [web3 id to])] - (when message - (@pending-message-callback :sent message)))))) - -(defn attempt-was-made! [web3 id to] - (debug :attempt-was-made id) - (swap! messages update-in [web3 id to] - (fn [{:keys [attempts] :as data}] - (assoc data :attempts (inc attempts) - :last-attempt (datetime/timestamp))))) - -(defn delivery-callback - [web3 post-error-callback {:keys [id requires-ack? to]} message] - (fn [error _] - (when error - (log/warn :shh-post-error error message) - (when post-error-callback - (post-error-callback error))) - (when-not error - (debug :delivery-callback) - (message-was-sent! web3 id to) - (when-not requires-ack? - (remove-pending-message! web3 id to))))) - -(s/def ::pos-int (s/and pos? int?)) -(s/def ::delivery-loop-ms-interval ::pos-int) -(s/def ::ack-not-received-s-interval ::pos-int) -(s/def ::max-attempts-number ::pos-int) -(s/def ::default-ttl ::pos-int) -(s/def ::send-online-s-interval ::pos-int) -(s/def ::online-message fn?) -(s/def ::post-error-callback fn?) - -(s/def ::delivery-options - (s/keys :req-un [::delivery-loop-ms-interval ::ack-not-received-s-interval - ::max-attempts-number ::default-ttl ::send-online-s-interval - ::post-error-callback] - :opt-un [::online-message])) - -(defn should-be-retransmitted? - "Checks if messages should be transmitted again." - [{:keys [ack-not-received-s-interval max-attempts-number]} - {:keys [was-sent? attempts last-attempt]}] - (if-not was-sent? - ;; message was not sent succesfully via web3.shh, but maybe - ;; better to do this only when we receive error from shh.post - ;; todo add some notification about network issues - (<= attempts (* 5 max-attempts-number)) - (and - ;; if message was not send less then max-attempts-number times - ;; continue attempts - (<= attempts max-attempts-number) - ;; check retransmission interval - (<= (+ last-attempt (* 1000 ack-not-received-s-interval)) (datetime/timestamp))))) - -(defn- check-ttl - [message message-type ttl-config default-ttl] - (update message :ttl #(or % ((keyword message-type) ttl-config) default-ttl))) - -(defn message-pending? - [web3 required-type required-to] - (some (fn [[_ messages]] - (some (fn [[_ {:keys [type to]}]] - (and (= type required-type) - (= to required-to))) - messages)) - (@messages web3))) - -(defn run-delivery-loop! - [web3 {:keys [delivery-loop-ms-interval default-ttl ttl-config - send-online-s-interval online-message post-error-callback - pow-target pow-time] - :as options}] - {:pre [(valid? ::delivery-options options)]} - (debug :run-delivery-loop!) - (let [previous-stop-flag @loop-state - stop? (atom false)] - ;; stop previous delivery loop if it exists - (when previous-stop-flag - (reset! previous-stop-flag true)) - ;; reset stop flag for a new loop - (reset! loop-state stop?) - ;; go go!!! - (debug :init-loop) - (async/go-loop [_ nil] - (doseq [[_ messages] (@messages web3)] - (doseq [[_ {:keys [id message to type] :as data}] messages] - ;; check each message asynchronously - (when (should-be-retransmitted? options data) - (try - (let [message' (-> message - (check-ttl type ttl-config default-ttl) - (assoc :powTarget pow-target - :powTime pow-time)) - callback (delivery-callback web3 post-error-callback data message')] - (t/post-message! web3 message' callback)) - (catch :default err - (log/error :post-message-error err)) - (finally - (attempt-was-made! web3 id to)))))) - (when-not @stop? - (recur (async/pending-message to)] - (when (get-in @messages key) - (swap! messages #(update-in % key assoc - :last-attempt 0 - :attempts 0))))) - -(defn reset-all-pending-messages! [] - (reset! messages {})) diff --git a/src/status_im/protocol/web3/filtering.cljs b/src/status_im/protocol/web3/filtering.cljs deleted file mode 100644 index 316702ea0e..0000000000 --- a/src/status_im/protocol/web3/filtering.cljs +++ /dev/null @@ -1,61 +0,0 @@ -(ns status-im.protocol.web3.filtering - (:require [status-im.protocol.web3.utils :as u] - [status-im.utils.config :as config] - [cljs.spec.alpha :as s] - [taoensso.timbre :as log])) - -;; XXX(oskarth): Perf issue to have one topic -;; See https://github.com/status-im/ideas/issues/55#issuecomment-355511183 -(def status-topic "0xaabb11ee") - -(defonce filters (atom {})) - -;; NOTE(oskarth): This has concerns for upgradability and chatting cross -;; versions. How can we do this breaking change gradually? - -;; NOTE(oskarth): Due to perf we don't want a single topic for all messages, -;; instead we want many. We need a way for user A and B to agree on which topics -;; to use. By using first 10 characters of the pub-key, we construct a topic -;; that requires no coordination for 1-1 chats. -(defn identity->topic [identity] - (apply str (take 10 identity))) - -(defn get-topics [& [identity]] - (if config/many-whisper-topics-enabled? - (do (log/info "FLAG: many-whisper-topics-enabled ON") - [(identity->topic identity)]) - (do (log/info "FLAG: many-whisper-topics-enabled OFF") - [status-topic]))) - -(s/def ::options (s/keys :opt-un [:message/to :message/topics])) - -(defn remove-filter! [web3 options] - (when-let [filter (get-in @filters [web3 options])] - (.stopWatching filter - (fn [error _] - (when error - (log/warn :remove-filter-error options error)))) - (log/debug :stop-watching options) - (swap! filters update web3 dissoc options))) - -(defn add-shh-filter! - [web3 {:keys [key type] :as options} callback] - (let [type (or type :asym) - options' (cond-> (dissoc options :key) - (= type :asym) (assoc :privateKeyID key) - (= type :sym) (assoc :symKeyID key)) - filter (.newMessageFilter (u/shh web3) (clj->js options') - callback - #(log/warn :add-filter-error (.stringify js/JSON (clj->js options')) %))] - (swap! filters assoc-in [web3 options] filter))) - -(defn add-filter! - [web3 {:keys [topics to] :as options} callback] - (remove-filter! web3 options) - (log/debug :add-filter options) - (add-shh-filter! web3 options callback)) - -(defn remove-all-filters! [] - (doseq [[web3 filters] @filters] - (doseq [options (keys filters)] - (remove-filter! web3 options)))) diff --git a/src/status_im/protocol/web3/inbox.cljs b/src/status_im/protocol/web3/inbox.cljs deleted file mode 100644 index dbc2270328..0000000000 --- a/src/status_im/protocol/web3/inbox.cljs +++ /dev/null @@ -1,81 +0,0 @@ -(ns status-im.protocol.web3.inbox - (:require [re-frame.core :as re-frame] - [status-im.native-module.core :as status] - [status-im.protocol.web3.utils :as web3.utils] - [taoensso.timbre :as log])) - -(def peers (atom #{})) -(def trusted-peers (atom #{})) - -;; NOTE(dmitryn) Expects JSON response like: -;; {"error": "msg"} or {"result": true} -(defn- parse-json [s] - (try - (let [res (-> s - js/JSON.parse - (js->clj :keywordize-keys true))] - ;; NOTE(dmitryn): AddPeer() may return {"error": ""} - ;; assuming empty error is a success response - ;; by transforming {"error": ""} to {:result true} - (if (and (:error res) - (= (:error res) "")) - {:result true} - res)) - (catch :default e - {:error (.-message e)}))) - -(defn- response-handler [error-fn success-fn] - (fn handle-response - ([response] - (let [{:keys [error result]} (parse-json response)] - (handle-response error result))) - ([error result] - (if error - (error-fn error) - (success-fn result))))) - -(defn add-peer [enode success-fn error-fn] - (if (@peers enode) - (success-fn true) - (status/add-peer enode - (response-handler error-fn (fn [result] - (swap! peers conj enode) - (success-fn result)))))) - -(defn registered-peer? [peers enode] - (let [peer-ids (set (map :id peers)) - enode-id (web3.utils/extract-enode-id enode)] - (contains? peer-ids enode-id))) - -(defn mark-trusted-peer [web3 enode peers success-fn error-fn] - (if (@trusted-peers enode) - (success-fn true) - (.markTrustedPeer (web3.utils/shh web3) - enode - (response-handler error-fn (fn [result] - (swap! trusted-peers conj enode) - (success-fn result)))))) - -;; TODO(dmitryn): use web3 instead of rpc call -(defn fetch-peers [success-fn error-fn] - (let [args {:jsonrpc "2.0" - :id 2 - :method "admin_peers" - :params []} - payload (.stringify js/JSON (clj->js args))] - (status/call-web3 payload - (response-handler error-fn success-fn)))) - -(defn request-messages [web3 wnode topic sym-key-id success-fn error-fn] - (log/info "offline inbox: sym-key-id" sym-key-id) - (let [opts {:mailServerPeer wnode - :topic topic - :symKeyID sym-key-id}] - (log/info "offline inbox: request-messages request") - (log/info "offline inbox: request-messages args" (pr-str opts)) - (.requestMessages (web3.utils/shh web3) - (clj->js opts) - (response-handler error-fn success-fn)))) - -(defn initialize! [web3] - (re-frame/dispatch [:initialize-offline-inbox web3])) diff --git a/src/status_im/protocol/web3/keys.cljs b/src/status_im/protocol/web3/keys.cljs deleted file mode 100644 index 898b130009..0000000000 --- a/src/status_im/protocol/web3/keys.cljs +++ /dev/null @@ -1,33 +0,0 @@ -(ns status-im.protocol.web3.keys - (:require [taoensso.timbre :as log])) - -(def status-key-password "status-key-password") -(def status-group-key-password "status-public-group-key-password") - -(defonce password->keys (atom {})) - -(defn- add-sym-key-from-password - [web3 password callback] - (.. web3 - -shh - (generateSymKeyFromPassword password callback))) - -(defn get-sym-key - "Memoizes expensive calls by password." - ([web3 password success-fn] - ;; TODO:(dmitryn) add proper error handling - ;; to other usages of get-sym-key fn - (get-sym-key web3 password success-fn #(log/error %))) - ([web3 password success-fn error-fn] - (if-let [key-id (get @password->keys password)] - (success-fn key-id) - (add-sym-key-from-password - web3 password - (fn [err res] - (if err - (error-fn err) - (do (swap! password->keys assoc password res) - (success-fn res)))))))) - -(defn reset-keys! [] - (reset! password->keys {})) diff --git a/src/status_im/protocol/web3/transport.cljs b/src/status_im/protocol/web3/transport.cljs deleted file mode 100644 index 1fe18e8974..0000000000 --- a/src/status_im/protocol/web3/transport.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.protocol.web3.transport - (:require [status-im.protocol.web3.utils :as u] - [cljs.spec.alpha :as s] - [status-im.protocol.validation :refer-macros [valid?]] - [taoensso.timbre :refer-macros [debug]])) - -(s/def :shh/payload string?) -(s/def :shh/powTarget number?) -(s/def :shh/powTime number?) -(s/def :shh/message - (s/keys - :req-un [:shh/payload :message/ttl :message/sig :message/topic - :shh/powTarget :shh/powTime])) - -(defn post-message! - [web3 message callback] - {:pre [(valid? :shh/message message)]} - (let [shh (u/shh web3)] - (.post shh (clj->js message) callback))) diff --git a/src/status_im/transport/core.cljs b/src/status_im/transport/core.cljs new file mode 100644 index 0000000000..704a35c667 --- /dev/null +++ b/src/status_im/transport/core.cljs @@ -0,0 +1,80 @@ +(ns ^{:doc "API to init and stop whisper messaging"} + status-im.transport.core + (:require [cljs.spec.alpha :as spec] + [re-frame.core :as re-frame] + [status-im.constants :as constants] + [status-im.transport.message.core :as message] + [status-im.transport.filters :as filters] + [status-im.transport.utils :as transport.utils] + [taoensso.timbre :as log] + [status-im.transport.inbox :as inbox] + [status-im.utils.handlers :as handlers] + [status-im.transport.db :as transport.db])) + +(defn init-whisper + "Initialises whisper protocol by: + - adding fixed shh discovery filter + - restoring existing symetric keys along with their unique filters + - (optionally) initializing offline inboxing" + [current-account-id {:keys [db web3] :as cofx}] + (log/debug :init-whisper) + (when-let [public-key (get-in db [:accounts/accounts current-account-id :public-key])] + (let [sym-key-added-callback (fn [chat-id sym-key sym-key-id] + (re-frame/dispatch [::sym-key-added {:chat-id chat-id + :sym-key sym-key + :sym-key-id sym-key-id}])) + topic (transport.utils/get-topic constants/contact-discovery)] + (handlers/merge-fx cofx + {:shh/add-discovery-filter {:web3 web3 + :private-key-id public-key + :topic topic} + :shh/restore-sym-keys {:web3 web3 + :transport (:transport/chats db) + :on-success sym-key-added-callback}} + (inbox/initialize-offline-inbox))))) + +;;TODO (yenda) remove once go implements persistence +;;Since symkeys are not persisted, we restore them via add sym-keys, +;;this is the callback that is called when a key has been restored for a particular chat. +;;it saves the sym-key-id in app-db to send messages later +;;and starts a filter to receive messages +(handlers/register-handler-fx + ::sym-key-added + (fn [{:keys [db]} [_ {:keys [chat-id sym-key sym-key-id]}]] + (let [web3 (:web3 db) + {:keys [topic] :as chat} (get-in db [:transport/chats chat-id])] + {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :data-store.transport/save {:chat-id chat-id + :chat (assoc chat :sym-key-id sym-key-id)} + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic topic + :chat-id chat-id}}))) + +;;TODO (yenda) uncomment and rework once go implements persistence +#_(doseq [[chat-id {:keys [sym-key-id topic] :as chat}] transport] + (when sym-key-id + (filters/add-filter! web3 + {:symKeyID sym-key-id + :topics [topic]} + (fn [js-error js-message] + (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message chat-id]))))) + +(defn unsubscribe-from-chat + "Unsubscribe from chat on transport layer" + [chat-id {:keys [db]}] + (let [filter (get-in db [:transport/chats chat-id :filter])] + {:db (update db :transport/chats dissoc chat-id) + :data-store.transport/delete chat-id + :shh/remove-filter filter})) + +(defn stop-whisper + "Stops whisper protocol by removing all existing shh filters + It is necessary to remove the filters because status-go there isn't currently a logout feature in status-go + to clean-up after logout. When logging out of account A and logging in account B, account B would receive + account A messages without this." + [{:keys [db]}] + (let [{:transport/keys [chats discovery-filter]} db + chat-filters (mapv :filter (vals chats)) + all-filters (conj chat-filters discovery-filter)] + {:shh/remove-filters all-filters})) diff --git a/src/status_im/transport/db.cljs b/src/status_im/transport/db.cljs new file mode 100644 index 0000000000..fa5ec99ea0 --- /dev/null +++ b/src/status_im/transport/db.cljs @@ -0,0 +1,34 @@ +(ns ^{:doc "DB spec and utils for the transport layer"} + status-im.transport.db + (:require-macros [status-im.utils.db :refer [allowed-keys]]) + (:require [cljs.spec.alpha :as spec])) + +;; required +(spec/def ::ack (spec/coll-of string? :kind vector?)) +(spec/def ::seen (spec/coll-of string? :kind vector?)) +(spec/def ::pending-ack (spec/coll-of string? :kind vector?)) +(spec/def ::pending-send (spec/coll-of string? :kind vector?)) +(spec/def ::topic string?) + +;; optional +(spec/def ::sym-key-id string?) +;;TODO (yenda) remove once go implements persistence +(spec/def ::sym-key string?) +(spec/def ::filter any?) + +(spec/def :transport/chat (allowed-keys :req-un [::ack ::seen ::pending-ack ::pending-send ::topic] + :opt-un [::sym-key-id ::sym-key ::filter])) + +(spec/def :transport/chats (spec/map-of :global/not-empty-string :transport/chat)) +(spec/def :transport/discovery-filter (spec/nilable any?)) + + +(defn create-chat + "Initialize datastructure for chat representation at the transport level + Currently only :topic is actually used" + [topic] + {:ack [] + :seen [] + :pending-ack [] + :pending-send [] + :topic topic}) diff --git a/src/status_im/transport/filters.cljs b/src/status_im/transport/filters.cljs new file mode 100644 index 0000000000..bc2c08d567 --- /dev/null +++ b/src/status_im/transport/filters.cljs @@ -0,0 +1,71 @@ +(ns ^{:doc "API for whisper filters"} + status-im.transport.filters + (:require [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] + [status-im.transport.utils :as utils] + [status-im.utils.config :as config] + [taoensso.timbre :as log])) + +(defn remove-filter! [filter] + (.stopWatching filter + (fn [error _] + (when error + (log/warn :remove-filter-error filter error)))) + (log/debug :stop-watching filter)) + +(defn add-shh-filter! + [web3 options callback] + (.newMessageFilter (utils/shh web3) (clj->js options) + callback + #(log/warn :add-filter-error (.stringify js/JSON (clj->js options)) %))) + +(defn add-filter! + [web3 {:keys [topics to] :as options} callback] + (let [options (if config/offline-inbox-enabled? + (assoc options :allowP2P true) + options)] + (log/debug :add-filter options) + (add-shh-filter! web3 options callback))) + +(re-frame/reg-fx + :shh/add-filter + (fn [{:keys [web3 sym-key-id topic chat-id]}] + (when-let [filter (add-filter! web3 + {:topics [topic] + :symKeyID sym-key-id} + (fn [js-error js-message] + (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message chat-id])))] + (re-frame/dispatch [::filter-added chat-id filter])))) + +(handlers/register-handler-db + ::filter-added + [re-frame/trim-v] + (fn [db [chat-id filter]] + (assoc-in db [:transport/chats chat-id :filter] filter))) + +(re-frame/reg-fx + :shh/add-discovery-filter + (fn [{:keys [web3 private-key-id topic]}] + (when-let [filter (add-filter! web3 + {:topics [topic] + :privateKeyID private-key-id} + (fn [js-error js-message] + (re-frame/dispatch [:protocol/receive-whisper-message js-error js-message])))] + (re-frame/dispatch [::discovery-filter-added filter])))) + +(handlers/register-handler-db + ::discovery-filter-added + [re-frame/trim-v] + (fn [db [filter]] + (assoc db :transport/discovery-filter filter))) + +(re-frame/reg-fx + :shh/remove-filter + (fn [filter] + (when filter (remove-filter! filter)))) + +(re-frame/reg-fx + :shh/remove-filters + (fn [filters] + (doseq [filter filters] + (remove-filter! filter)))) diff --git a/src/status_im/transport/handlers.cljs b/src/status_im/transport/handlers.cljs new file mode 100644 index 0000000000..de13057ccf --- /dev/null +++ b/src/status_im/transport/handlers.cljs @@ -0,0 +1,37 @@ +(ns ^{:doc "Events for message handling"} + status-im.transport.handlers + (:require [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] + [status-im.transport.message.core :as message] + [status-im.transport.core :as transport] + [status-im.chat.models :as models.chat] + [status-im.utils.datetime :as datetime] + [taoensso.timbre :as log] + [status-im.transport.utils :as transport.utils] + [cljs.reader :as reader] + [status-im.transport.message.transit :as transit] + [status-im.transport.shh :as shh] + [status-im.transport.filters :as filters])) + +(handlers/register-handler-fx + :protocol/receive-whisper-message + [re-frame/trim-v] + (fn [cofx [js-error js-message chat-id]] + (let [{:keys [payload sig]} (js->clj js-message :keywordize-keys true) + status-message (-> payload + transport.utils/to-utf8 + transit/deserialize)] + (when (and sig status-message) + (message/receive status-message (or chat-id sig) sig cofx))))) + +(handlers/register-handler-fx + :protocol/send-status-message-success + [re-frame/trim-v] + (fn [{:keys [db] :as cofx} [_ resp]] + (log/debug :send-status-message-success resp))) + +(handlers/register-handler-fx + :protocol/send-status-message-error + [re-frame/trim-v] + (fn [{:keys [db] :as cofx} [err]] + (log/error :send-status-message-error err))) diff --git a/src/status_im/transport/inbox.cljs b/src/status_im/transport/inbox.cljs new file mode 100644 index 0000000000..063c238bf9 --- /dev/null +++ b/src/status_im/transport/inbox.cljs @@ -0,0 +1,191 @@ +(ns ^{:doc "Offline inboxing events and API"} + status-im.transport.inbox + (:require [re-frame.core :as re-frame] + [status-im.native-module.core :as status] + [status-im.utils.handlers :as handlers] + [status-im.transport.utils :as web3.utils] + [status-im.utils.config :as config] + [taoensso.timbre :as log])) + +(defn- parse-json + ;; NOTE(dmitryn) Expects JSON response like: + ;; {"error": "msg"} or {"result": true} + [s] + (try + (let [res (-> s + js/JSON.parse + (js->clj :keywordize-keys true))] + ;; NOTE(dmitryn): AddPeer() may return {"error": ""} + ;; assuming empty error is a success response + ;; by transforming {"error": ""} to {:result true} + (if (and (:error res) + (= (:error res) "")) + {:result true} + res)) + (catch :default e + {:error (.-message e)}))) + +(defn- response-handler [error-fn success-fn] + (fn handle-response + ([response] + (let [{:keys [error result]} (parse-json response)] + (handle-response error result))) + ([error result] + (if error + (error-fn error) + (success-fn result))))) + +(defn initialize-offline-inbox + "Initialises offline inbox by producing `::add-peer` effect if inboxing enabled in config, + then the event chan is: + add-peer -> fetch-peers -> check-peer-added -> mark-trusted-peer -> get-sym-key -> request-messages" + [{:keys [db]}] + (when config/offline-inbox-enabled? + (let [wnode-id (get db :inbox/wnode) + wnode (get-in db [:inbox/wnodes wnode-id :address])] + (log/info "offline inbox: initialize") + {::add-peer {:wnode wnode}}))) + +(defn add-peer [enode success-fn error-fn] + (status/add-peer enode (response-handler error-fn success-fn))) + +(defn fetch-peers + ;; https://github.com/ethereum/go-ethereum/wiki/Management-APIs#admin_peers + ;; retrieves all the information known about the connected remote nodes + ;; TODO(dmitryn): use web3 instead of rpc call + [success-fn error-fn] + (let [args {:jsonrpc "2.0" + :id 2 + :method "admin_peers" + :params []} + payload (.stringify js/JSON (clj->js args))] + (status/call-web3 payload (response-handler error-fn success-fn)))) + +(defn registered-peer? [peers enode] + (let [peer-ids (into #{} (map :id) peers) + enode-id (web3.utils/extract-enode-id enode)] + (contains? peer-ids enode-id))) + +(defn mark-trusted-peer [web3 enode peers success-fn error-fn] + (.markTrustedPeer (web3.utils/shh web3) + enode + (fn [err resp] + (if-not err + (success-fn resp) + (error-fn err))))) + +(defn request-messages [web3 wnode topics to from sym-key-id success-fn error-fn] + (log/info "offline inbox: sym-key-id" sym-key-id) + (let [opts (merge {:mailServerPeer wnode + :symKeyID sym-key-id} + (when from {:from from}) + (when to {:to to}))] + (log/info "offline inbox: request-messages request for topics " topics) + (doseq [topic topics] + (let [opts (assoc opts :topic topic)] + (log/info "offline inbox: request-messages args" (pr-str opts)) + (.requestMessages (web3.utils/shh web3) + (clj->js opts) + (fn [err resp] + (if-not err + (success-fn resp) + (error-fn err topic)))))))) + +(defn get-wnode [db] + (let [wnode-id (get db :inbox/wnode)] + (get-in db [:inbox/wnodes wnode-id :address]))) + +(re-frame/reg-fx + ::add-peer + (fn [{:keys [wnode]}] + (add-peer wnode + #(re-frame/dispatch [::fetch-peers %]) + #(log/error "offline inbox: add-peer error" %)))) + +(re-frame/reg-fx + ::fetch-peers + (fn [retries] + (fetch-peers #(re-frame/dispatch [::check-peer-added % retries]) + #(log/error "offline inbox: fetch-peers error" %)))) + +(re-frame/reg-fx + ::mark-trusted-peer + (fn [{:keys [wnode web3 peers]}] + (mark-trusted-peer web3 + wnode + peers + #(re-frame/dispatch [::get-sym-key %]) + #(log/error "offline inbox: mark-trusted-peer error" % wnode)))) + +(re-frame/reg-fx + ::request-messages + (fn [{:keys [wnode topics to from sym-key-id web3]}] + (request-messages web3 + wnode + topics + to + from + sym-key-id + #(log/info "offline inbox: request-messages response" %) + #(log/error "offline inbox: request-messages error" %1 %2 to from)))) + +;;;; Handlers + +(handlers/register-handler-fx + ::fetch-peers + ;; This event fetches the list of peers + ;; We want it to check if the node has been added + (fn [_ [_ retries]] + {::fetch-peers (or retries 0)})) + +(handlers/register-handler-fx + ::check-peer-added + ;; We check if the wnode is part of the peers list + ;; if not we dispatch a new fetch-peer event for later + (fn [{:keys [db]} [_ peers retries]] + (let [web3 (:web3 db) + wnode (get-wnode db)] + (log/info "offline inbox: fetch-peers response" peers) + (if (registered-peer? peers wnode) + {::mark-trusted-peer {:web3 web3 + :wnode wnode + :peers peers}} + (do + (log/info "Peer" wnode "is not registered. Retrying fetch peers.") + (let [delay (if (< retries 3) 300 5000)] + (if (> retries 10) + (log/error "Could not connect to mailserver") + {:dispatch-later [{:ms delay :dispatch [::fetch-peers (inc retries)]}]}))))))) + +(handlers/register-handler-fx + ::get-sym-key + ;; TODO(yenda): using core async flow this event can be done in parallel + ;; with add-peer + (fn [{:keys [db]} [_ response]] + (let [web3 (:web3 db) + wnode (get-wnode db) + password (:inbox/password db)] + (log/info "offline inbox: mark-trusted-peer response" wnode response) + {:shh/generate-sym-key-from-password {:password password + :web3 web3 + :on-success (fn [_ sym-key-id] + (re-frame/dispatch [::request-messages sym-key-id])) + :on-error #(log/error "offline inbox: get-sym-key error" %)}}))) + +(handlers/register-handler-fx + ::request-messages + ;; TODO(yenda): we want to request-message once per topic and for specific timespan so + ;; we want a plural version of this function that does the right thing + (fn [{:keys [db]} [_ sym-key-id]] + (log/info "offline inbox: get-sym-key response") sym-key-id + (let [web3 (:web3 db) + wnode (get-wnode db) + topics (map #(:topic %) (vals (:transport/chats db))) + to nil + from nil] + {::request-messages {:wnode wnode + :topics topics + :to to + :from from + :sym-key-id sym-key-id + :web3 web3}}))) diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs new file mode 100644 index 0000000000..38221ddd8b --- /dev/null +++ b/src/status_im/transport/message/core.cljs @@ -0,0 +1,7 @@ +(ns ^{:doc "Definition of the StatusMessage protocol"} + status-im.transport.message.core) + +(defprotocol StatusMessage + "Protocol for the messages that are sent through the transport layer" + (send [this chat-id cofx] "Method producing all effects necessary for sending the message record") + (receive [this chat-id signature cofx] "Method producing all effects necessary for receiving the message record")) diff --git a/src/status_im/transport/message/transit.cljs b/src/status_im/transport/message/transit.cljs new file mode 100644 index 0000000000..536b2253ea --- /dev/null +++ b/src/status_im/transport/message/transit.cljs @@ -0,0 +1,123 @@ +(ns ^{:doc "Transit custom readers and writers, required when adding a new record implementing StatusMessage protocol"} + status-im.transport.message.transit + (:require [status-im.transport.message.v1.contact :as v1.contact] + [status-im.transport.message.v1.protocol :as v1.protocol] + [status-im.transport.message.v1.group-chat :as v1.group-chat] + [cognitect.transit :as transit])) + +;; When adding a new reccord implenting the StatusMessage protocol it is required to implement: +;; - a handler that will turn the clojure record into a javascript datastructure. +;; - a reader that will turn the javascript datastructure back into a clojure record. + +;; Use the existing types as exemples of how this is done + +;; +;; Writer handlers +;; + +;; Each writer defines a tag and a representation +;; The tag will determine which reader is used to recreate the clojure record +;; When migrating a particular record, it is important to use a different type and still handle the previous +;; gracefully for compatibility +(deftype NewContactKeyHandler [] + Object + (tag [this v] "c1") + (rep [this {:keys [sym-key topic message]}] + #js [sym-key topic message])) + +(deftype ContactRequestHandler [] + Object + (tag [this v] "c2") + (rep [this {:keys [name profile-image address fcm-token]}] + #js [name profile-image address fcm-token])) + +(deftype ContactRequestConfirmedHandler [] + Object + (tag [this v] "c3") + (rep [this {:keys [name profile-image address fcm-token]}] + #js [name profile-image address fcm-token])) + +(deftype ContactUpdateHandler [] + Object + (tag [this v] "c6") + (rep [this {:keys [name profile-image]}] + #js [name profile-image])) + +(deftype MessageHandler [] + Object + (tag [this v] "c4") + (rep [this {:keys [content content-type message-type to-clock-value timestamp]}] + #js [content content-type message-type to-clock-value timestamp])) + +(deftype MessagesSeenHandler [] + Object + (tag [this v] "c5") + (rep [this {:keys [message-ids]}] + (clj->js message-ids))) + +(deftype NewGroupKeyHandler [] + Object + (tag [this v] "g1") + (rep [this {:keys [chat-id sym-key message]}] + #js [chat-id sym-key message])) + +(deftype GroupAdminUpdateHandler [] + Object + (tag [this v] "g2") + (rep [this {:keys [chat-name participants]}] + #js [chat-name participants])) + +(deftype GroupLeaveHandler [] + Object + (tag [this v] "g3") + (rep [this _] + (clj->js nil))) + +(def writer (transit/writer :json + {:handlers + {v1.contact/NewContactKey (NewContactKeyHandler.) + v1.contact/ContactRequest (ContactRequestHandler.) + v1.contact/ContactRequestConfirmed (ContactRequestConfirmedHandler.) + v1.contact/ContactUpdate (ContactUpdateHandler.) + v1.protocol/Message (MessageHandler.) + v1.protocol/MessagesSeen (MessagesSeenHandler.) + v1.group-chat/NewGroupKey (NewGroupKeyHandler.) + v1.group-chat/GroupAdminUpdate (GroupAdminUpdateHandler.) + v1.group-chat/GroupLeave (GroupLeaveHandler.)}})) + +;; +;; Reader handlers +;; + +;; Here we only need to call the record with the arguments parsed from the clojure datastructures +(def reader (transit/reader :json + {:handlers + {"c1" (fn [[sym-key topic message]] + (v1.contact/NewContactKey. sym-key topic message)) + "c2" (fn [[name profile-image address fcm-token]] + (v1.contact/ContactRequest. name profile-image address fcm-token)) + "c3" (fn [[name profile-image address fcm-token]] + (v1.contact/ContactRequestConfirmed. name profile-image address fcm-token)) + "c4" (fn [[content content-type message-type to-clock-value timestamp]] + (v1.protocol/Message. content content-type message-type to-clock-value timestamp)) + "c5" (fn [message-ids] + (v1.protocol/MessagesSeen. message-ids)) + "c6" (fn [[name profile-image]] + (v1.contact/ContactUpdate. name profile-image)) + "g1" (fn [[chat-id sym-key message]] + (v1.group-chat/NewGroupKey. chat-id sym-key message)) + "g2" (fn [[chat-name participants]] + (v1.group-chat/GroupAdminUpdate. chat-name participants)) + "g3" (fn [_] + (v1.group-chat/GroupLeave.))}})) + + +(defn serialize + "Serializes a record implementing the StatusMessage protocol using the custom writers" + [o] + (transit/write writer o)) + +(defn deserialize + "Deserializes a record implementing the StatusMessage protocol using the custom readers" + [o] + (try (transit/read reader o) (catch :default e nil))) diff --git a/src/status_im/transport/message/v1/contact.cljs b/src/status_im/transport/message/v1/contact.cljs new file mode 100644 index 0000000000..f7c76a5bd6 --- /dev/null +++ b/src/status_im/transport/message/v1/contact.cljs @@ -0,0 +1,134 @@ +(ns ^{:doc "Contact request and update API"} + status-im.transport.message.v1.contact + (:require [re-frame.core :as re-frame] + [status-im.transport.message.core :as message] + [status-im.transport.message.v1.protocol :as protocol] + [status-im.transport.utils :as transport.utils] + [status-im.ui.screens.contacts.core :as contacts] + [status-im.utils.handlers :as handlers])) + +(defrecord NewContactKey [sym-key topic message] + message/StatusMessage + (send [this chat-id cofx] + (protocol/send-with-pubkey {:chat-id chat-id + :payload this} + cofx)) + (receive [this chat-id signature cofx] + (let [on-success (fn [sym-key sym-key-id] + (re-frame/dispatch [::add-new-sym-key {:sym-key-id sym-key-id + :sym-key sym-key + :chat-id chat-id + :topic topic + :message message}]))] + (handlers/merge-fx cofx + {:shh/add-new-sym-key {:web3 (get-in cofx [:db :web3]) + :sym-key sym-key + :on-success on-success}} + (protocol/init-chat chat-id topic))))) + +(defrecord ContactRequest [name profile-image address fcm-token] + message/StatusMessage + (send [this chat-id {:keys [db random-id] :as cofx}] + (let [message-id (transport.utils/message-id this) + topic (transport.utils/get-topic random-id) + on-success (fn [sym-key sym-key-id] + (re-frame/dispatch [::send-new-sym-key {:sym-key-id sym-key-id + :sym-key sym-key + :chat-id chat-id + :topic topic + :message this}]))] + (handlers/merge-fx cofx + {:shh/get-new-sym-key {:web3 (:web3 db) + :on-success on-success}} + (protocol/init-chat chat-id topic) + #_(protocol/requires-ack message-id chat-id)))) + (receive [this chat-id signature {:keys [db] :as cofx}] + (let [message-id (transport.utils/message-id this)] + (when (protocol/is-new? message-id) + (handlers/merge-fx cofx + #_(protocol/ack message-id chat-id) + (contacts/receive-contact-request signature + this)))))) + +(defrecord ContactRequestConfirmed [name profile-image address fcm-token] + message/StatusMessage + (send [this chat-id cofx] + (let [message-id (transport.utils/message-id this)] + (handlers/merge-fx cofx + #_(protocol/requires-ack message-id chat-id) + (protocol/send {:chat-id chat-id + :payload this})))) + (receive [this chat-id signature cofx] + (let [message-id (transport.utils/message-id this)] + (when (protocol/is-new? message-id) + (handlers/merge-fx cofx + #_(protocol/ack message-id chat-id) + (contacts/receive-contact-request-confirmation signature + this)))))) + +(defrecord ContactUpdate [name profile-image] + message/StatusMessage + (send [this _ {:keys [db] :as cofx}] + (let [message-id (transport.utils/message-id this) + public-keys (remove nil? (map :public-key (vals (:contacts/contacts db))))] + (handlers/merge-fx cofx + (protocol/multi-send-with-pubkey {:public-keys public-keys + :payload this})))) + (receive [this chat-id signature cofx] + (let [message-id (transport.utils/message-id this)] + (when (protocol/is-new? message-id) + (handlers/merge-fx cofx + (contacts/receive-contact-update chat-id + signature + this)))))) + +(handlers/register-handler-fx + ::send-new-sym-key + (fn [{:keys [db random-id] :as cofx} [_ {:keys [chat-id topic message sym-key sym-key-id]}]] + (let [{:keys [web3 current-public-key]} db + chat-transport-info (-> (get-in db [:transport/chats chat-id]) + (assoc :sym-key-id sym-key-id) + ;;TODO (yenda) remove once go implements persistence + (assoc :sym-key sym-key))] + (handlers/merge-fx cofx + {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic topic + :chat-id chat-id} + :data-store.transport/save {:chat-id chat-id + :chat chat-transport-info}} + (message/send (NewContactKey. sym-key topic message) + chat-id))))) + +(handlers/register-handler-fx + ::add-new-sym-key + (fn [{:keys [db] :as cofx} [_ {:keys [sym-key-id sym-key chat-id topic message]}]] + (let [{:keys [web3 current-public-key]} db + chat-transport-info (-> (get-in db [:transport/chats chat-id]) + (assoc :sym-key-id sym-key-id) + ;;TODO (yenda) remove once go implements persistence + (assoc :sym-key sym-key))] + (handlers/merge-fx cofx + {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic topic + :chat-id chat-id} + :data-store.transport/save {:chat-id chat-id + :chat chat-transport-info}} + (message/receive message chat-id chat-id))))) + +#_(handlers/register-handler-fx + :send-test-message + (fn [cofx [this timer chat-id n]] + (if (zero? n) + (println "Time: " (str (- (inst-ms (js/Date.)) @timer))) + (handlers/merge-fx cofx + {:dispatch [this timer chat-id (dec n)]} + (message/send (protocol/map->Message {:content (str n) + :content-type "text/plain" + :message-type :user-message + :clock-value n + :timestamp (str (inst-ms (js/Date.)))}) + chat-id))))) diff --git a/src/status_im/transport/message/v1/group_chat.cljs b/src/status_im/transport/message/v1/group_chat.cljs new file mode 100644 index 0000000000..45eb792e0c --- /dev/null +++ b/src/status_im/transport/message/v1/group_chat.cljs @@ -0,0 +1,179 @@ +(ns ^{:doc "Group chat API"} + status-im.transport.message.v1.group-chat + (:require [clojure.set :as set] + [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] + [status-im.transport.message.core :as message] + [status-im.i18n :as i18n] + [status-im.ui.screens.group.core :as group] + [status-im.chat.models :as models.chat] + [status-im.chat.models.message :as models.message] + [status-im.transport.core :as transport] + [status-im.transport.message.v1.protocol :as protocol] + [status-im.transport.utils :as transport.utils])) + + +;; NOTE: We ignore the chat-id from the send and receive method. +;; The chat-id is usually deduced from the filter the message comes from but not in that case because it is sent +;; individually to each participant of the group. +;; In order to be able to determine which group the message belongs to the chat-id is therefore +;; passed in the message itself +(defrecord NewGroupKey [chat-id sym-key message] + message/StatusMessage + (send [this _ cofx] + (let [public-keys (map :identity (get-in cofx [:db :chats chat-id :contacts]))] + (protocol/multi-send-with-pubkey {:public-keys public-keys + :chat-id chat-id + :payload this} + cofx))) + (receive [this _ signature {:keys [db] :as cofx}] + (handlers/merge-fx cofx + {:shh/add-new-sym-key {:web3 (:web3 db) + :sym-key sym-key + :on-success (fn [sym-key sym-key-id] + (re-frame/dispatch [::add-new-sym-key {:chat-id chat-id + :sym-key sym-key + :sym-key-id sym-key-id + :message message}]))}} + (protocol/init-chat chat-id)))) + +(defn- user-is-group-admin? [chat-id cofx] + (= (get-in cofx [:db :chats chat-id :group-admin]) + (get-in cofx [:db :current-public-key]))) + +(defn- send-new-group-key [message chat-id cofx] + (when (user-is-group-admin? chat-id cofx) + {:shh/get-new-sym-key {:web3 (get-in cofx [:db :web3]) + :on-success (fn [sym-key sym-key-id] + (re-frame/dispatch [::send-new-sym-key {:chat-id chat-id + :sym-key sym-key + :sym-key-id sym-key-id + :message message}]))}})) + +(defn- prepare-system-message [admin-name added-participants removed-participants contacts] + (let [added-participants-names (map #(get-in contacts [% :name] %) added-participants) + removed-participants-names (map #(get-in contacts [% :name] %) removed-participants)] + (cond + (and added-participants 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))) + + added-participants + (str admin-name " " (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names))) + + removed-participants + (str admin-name " " (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names)))))) + +(defn- init-chat-if-new [chat-id cofx] + (if (nil? (get-in cofx [:db :transport/chats chat-id])) + (protocol/init-chat chat-id cofx))) + +(defn- participants-diff [existing-participants-set new-participants-set] + {:removed (set/difference existing-participants-set new-participants-set) + :added (set/difference new-participants-set existing-participants-set)}) + +(defrecord GroupAdminUpdate [chat-name participants] + message/StatusMessage + (send [this chat-id cofx] + (handlers/merge-fx cofx + (init-chat-if-new chat-id) + (send-new-group-key this chat-id))) + (receive [this chat-id signature {:keys [now db] :as cofx}] + (let [me (:current-public-key db)] + ;; we have to check if we already have a chat, or it's a new one + (if-let [{:keys [group-admin contacts] :as chat} (get-in db [:chats chat-id])] + ;; update for existing group chat + (when (= signature group-admin) ;; make sure that admin is the one making changes + (let [{:keys [removed added]} (participants-diff (set contacts) (set participants)) + admin-name (or (get-in cofx [db :contacts/contacts group-admin :name]) + group-admin) + message-id (transport.utils/message-id this)] + (if (removed me) ;; we were removed + (handlers/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id message-id now + (str admin-name " " (i18n/label :t/removed-from-chat)))) + (models.chat/update-chat {:chat-id chat-id + :removed-from-at now + :is-active false}) + (transport/unsubscribe-from-chat chat-id)) + (handlers/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id message-id now + (prepare-system-message admin-name + added + removed + (:contacts/contacts db)))) + (group/participants-added chat-id added) + (group/participants-removed chat-id removed))))) + ;; first time we hear about chat -> create it if we are among participants + (when (get (set participants) me) + (models.chat/add-group-chat chat-id chat-name signature participants cofx)))))) + +(defrecord GroupLeave [] + message/StatusMessage + (send [this chat-id cofx] + (protocol/send {:chat-id chat-id + :payload this + :success-event [::unsubscribe-from-chat chat-id]} + cofx)) + (receive [this chat-id signature {:keys [db now] :as cofx}] + (let [message-id (transport.utils/message-id this) + me (:current-public-key db) + participant-leaving-name (or (get-in db [:contacts/contacts signature :name]) + signature)] + (when-not (= me signature) + (handlers/merge-fx cofx + (models.message/receive + (models.message/system-message chat-id message-id now + (str participant-leaving-name " " (i18n/label :t/left)))) + (group/participants-removed chat-id #{signature}) + (send-new-group-key nil chat-id)))))) + +(handlers/register-handler-fx + ::unsubscribe-from-chat + [re-frame/trim-v] + (fn [cofx [chat-id]] + (transport/unsubscribe-from-chat chat-id cofx))) + +(handlers/register-handler-fx + ::send-new-sym-key + [re-frame/trim-v] + ;; this is the event that is called when we want to send a message that required first + ;; some async operations + (fn [{:keys [db] :as cofx} [{:keys [chat-id message sym-key sym-key-id]}]] + (let [{:keys [web3]} db] + (handlers/merge-fx cofx + {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic (transport.utils/get-topic chat-id) + :chat-id chat-id} + :data-store.transport/save {:chat-id chat-id + :chat (-> (get-in db [:transport/chats chat-id]) + (assoc :sym-key-id sym-key-id) + ;;TODO (yenda) remove once go implements persistence + (assoc :sym-key sym-key))}} + (message/send (NewGroupKey. chat-id sym-key message) chat-id))))) + +(handlers/register-handler-fx + ::add-new-sym-key + [re-frame/trim-v] + (fn [{:keys [db] :as cofx} [{:keys [sym-key-id sym-key chat-id message]}]] + (let [{:keys [web3 current-public-key]} db + fx {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic (transport.utils/get-topic chat-id) + :chat-id chat-id} + :data-store.transport/save {:chat-id chat-id + :chat (-> (get-in db [:transport/chats chat-id]) + (assoc :sym-key-id sym-key-id) + ;;TODO (yenda) remove once go implements persistence + (assoc :sym-key sym-key))}}] + ;; if new sym-key is wrapping some message, call receive on it as well, if not just update the transport layer + (if message + (handlers/merge-fx cofx fx (message/receive message chat-id chat-id)) + fx)))) diff --git a/src/status_im/transport/message/v1/protocol.cljs b/src/status_im/transport/message/v1/protocol.cljs new file mode 100644 index 0000000000..d8bd456cef --- /dev/null +++ b/src/status_im/transport/message/v1/protocol.cljs @@ -0,0 +1,116 @@ +(ns ^{:doc "Protocol API and protocol utils"} + status-im.transport.message.v1.protocol + (:require [status-im.utils.config :as config] + [status-im.constants :as constants] + [status-im.chat.core :as chat] + [status-im.transport.message-cache :as message-cache] + [status-im.transport.db :as transport.db] + [status-im.transport.core :as transport] + [status-im.transport.message.core :as message] + [status-im.transport.utils :as transport.utils])) + +(def ^:private whisper-opts + {:ttl 10 ;; ttl of 10 sec + :powTarget config/pow-target + :powTime config/pow-time}) + +(defn init-chat + "Initialises chat on protocol layer. + 2 arities are provided, 2arg arity initialises the chat with topic derived from first argument (`chat-id`), + using the 3arg arity, you can specify the topic as well (second argument)" + ([chat-id cofx] + (init-chat chat-id (transport.utils/get-topic chat-id) cofx)) + ([chat-id topic {:keys [db] :as cofx}] + {:db (assoc-in db [:transport/chats chat-id] (transport.db/create-chat topic))})) + +(defn is-new? + "Determines if given message-id already exists in in-memory message cache" + [message-id] + (when-not (message-cache/exists? message-id) + (message-cache/add! message-id))) + +#_(defn requires-ack [message-id chat-id {:keys [db] :as cofx}] + {:db (update-in db [:transport/chats chat-id :pending-ack] conj message-id)}) + +#_(defn ack [message-id chat-id {:keys [db] :as cofx}] + {:db (update-in db [:transport/chats chat-id :ack] conj message-id)}) + +(defn send + "Sends the payload using symetric key and topic from db (looked up by `chat-id`)" + [{:keys [payload chat-id success-event]} {:keys [db] :as cofx}] + ;; we assume that the chat contains the contact public-key + (let [{:keys [current-public-key web3]} db + {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id])] + {:shh/post {:web3 web3 + :success-event success-event + :message (merge {:sig current-public-key + :symKeyID sym-key-id + :payload payload + :topic topic} + whisper-opts)}})) + +(defn send-with-pubkey + "Sends the payload using asymetric key (`:current-public-key` in db) and fixed discovery topic" + [{:keys [payload chat-id success-event]} {:keys [db] :as cofx}] + (let [{:keys [current-public-key web3]} db] + {:shh/post {:web3 web3 + :success-event success-event + :message (merge {:sig current-public-key + :pubKey chat-id + :payload payload + :topic (transport.utils/get-topic constants/contact-discovery)} + whisper-opts)}})) + +(defn- prepare-recipients [public-keys db] + (map (fn [public-key] + (select-keys (get-in db [:transport/chats public-key]) [:topic :sym-key-id])) + public-keys)) + +(defn multi-send-with-pubkey + "Sends payload to multiple participants selected by `:public-keys` key." + [{:keys [payload public-keys success-event]} {:keys [db] :as cofx}] + (let [{:keys [current-public-key web3]} db + recipients (prepare-recipients public-keys db)] + {:shh/multi-post {:web3 web3 + :success-event success-event + :recipients recipients + :message (merge {:sig current-public-key + :payload payload} + whisper-opts)}})) + +;; TODO currently not used +(defrecord Ack [message-ids] + message/StatusMessage + (send [this cofx chat-id]) + (receive [this db chat-id sig])) + +(defrecord Seen [message-ids] + message/StatusMessage + (send [this cofx chat-id]) + (receive [this cofx chat-id sig])) + +(defrecord Message [content content-type message-type to-clock-value timestamp] + message/StatusMessage + (send [this chat-id cofx] + (send {:chat-id chat-id + :payload this + :success-event [:update-message-status + chat-id + (transport.utils/message-id this) + (get-in cofx [:db :current-public-key]) + :sent]} + cofx)) + (receive [this chat-id signature cofx] + {:dispatch [:chat-received-message/add (assoc (into {} this) + :message-id (transport.utils/message-id this) + :chat-id chat-id + :from signature)]})) + +(defrecord MessagesSeen [message-ids] + message/StatusMessage + (send [this chat-id cofx] + (send {:chat-id chat-id + :payload this} + cofx)) + (receive [this chat-id signature cofx] + (chat/receive-seen chat-id signature (:message-ids this) cofx))) diff --git a/src/status_im/transport/message/v1/public_chat.cljs b/src/status_im/transport/message/v1/public_chat.cljs new file mode 100644 index 0000000000..f5c13120b1 --- /dev/null +++ b/src/status_im/transport/message/v1/public_chat.cljs @@ -0,0 +1,36 @@ +(ns ^{:doc "Public chat API"} + status-im.transport.message.v1.public-chat + (:require [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] + [status-im.transport.message.core :as message] + [status-im.transport.message.v1.protocol :as protocol] + [status-im.transport.utils :as transport.utils])) + +(defn join-public-chat + "Function producing all protocol level effects necessary for joining public chat identified by chat-id" + [chat-id {:keys [db] :as cofx}] + (let [on-success (fn [sym-key sym-key-id] + (re-frame/dispatch [::add-new-sym-key {:chat-id chat-id + :sym-key sym-key + :sym-key-id sym-key-id}]))] + (handlers/merge-fx cofx + {:shh/generate-sym-key-from-password {:web3 (:web3 db) + :password chat-id + :on-success on-success}} + (protocol/init-chat chat-id)))) + +(handlers/register-handler-fx + ::add-new-sym-key + (fn [{:keys [db] :as cofx} [_ {:keys [sym-key-id sym-key chat-id]}]] + (let [{:keys [web3]} db] + (handlers/merge-fx cofx + {:db (assoc-in db [:transport/chats chat-id :sym-key-id] sym-key-id) + :shh/add-filter {:web3 web3 + :sym-key-id sym-key-id + :topic (transport.utils/get-topic chat-id) + :chat-id chat-id} + :data-store.transport/save {:chat-id chat-id + :chat (-> (get-in db [:transport/chats chat-id]) + (assoc :sym-key-id sym-key-id) + ;;TODO (yenda) remove once go implements persistence + (assoc :sym-key sym-key))}})))) diff --git a/src/status_im/transport/message_cache.cljs b/src/status_im/transport/message_cache.cljs new file mode 100644 index 0000000000..39aee88988 --- /dev/null +++ b/src/status_im/transport/message_cache.cljs @@ -0,0 +1,19 @@ +(ns ^{:doc "Message caching API for message deduplication"} + status-im.transport.message-cache + (:refer-clojure :exclude [exists?])) + +(defonce messages-set (atom #{})) + +(defn init! + [messages] + (reset! messages-set (set (map :message-id messages)))) + +(defn add! + [message-id] + (when message-id + (swap! messages-set conj message-id))) + +(defn exists? + [message-id] + (when message-id + (@messages-set message-id))) diff --git a/src/status_im/transport/shh.cljs b/src/status_im/transport/shh.cljs new file mode 100644 index 0000000000..6defd5aa41 --- /dev/null +++ b/src/status_im/transport/shh.cljs @@ -0,0 +1,169 @@ +(ns ^{:doc "Whisper API and events for managing keys and posting messages"} + status-im.transport.shh + (:require [taoensso.timbre :as log] + [re-frame.core :as re-frame] + [status-im.transport.utils :as transport.utils] + [status-im.transport.message.transit :as transit])) + +(defn get-new-key-pair [{:keys [web3 on-success on-error]}] + (if web3 + (.. web3 + -shh + (newKeyPair (fn [err resp] + (if-not err + (on-success resp) + (on-error err))))) + (on-error "web3 not available."))) + +(re-frame/reg-fx + :shh/get-new-key-pair + (fn [{:keys [web3 success-event error-event]}] + (get-new-key-pair {:web3 web3 + :on-success #(re-frame/dispatch [success-event %]) + :on-error #(re-frame/dispatch [error-event %])}))) + +(defn get-public-key [{:keys [web3 key-pair-id on-success on-error]}] + (if (and web3 key-pair-id) + (.. web3 + -shh + (getPublicKey key-pair-id (fn [err resp] + (if-not err + (on-success resp) + (on-error err))))) + (on-error "web3 or key-pair id not available."))) + +(re-frame/reg-fx + :shh/get-public-key + (fn [{:keys [web3 key-pair-id success-event error-event]}] + (get-public-key {:web3 web3 + :key-pair-id key-pair-id + :on-success #(re-frame/dispatch [success-event %]) + :on-error #(re-frame/dispatch [error-event %])}))) + +(defn generate-sym-key-from-password + [{:keys [web3 password on-success on-error]}] + (.. web3 + -shh + (generateSymKeyFromPassword password (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))))) + +(defn post-message + [{:keys [web3 whisper-message on-success on-error]}] + (.. web3 + -shh + (post (clj->js whisper-message) (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))))) + +(re-frame/reg-fx + :shh/post + (fn [{:keys [web3 message success-event error-event] + :or {error-event :protocol/send-status-message-error}}] + (post-message {:web3 web3 + :whisper-message (update message :payload (comp transport.utils/from-utf8 + transit/serialize)) + :on-success (if success-event + #(re-frame/dispatch success-event) + #(log/debug :shh/post-success)) + :on-error #(re-frame/dispatch [error-event %])}))) + +;; This event params contain a recipients key because it's a vector of map with public-key and topic keys. +;; the :shh/post event has public-key and topic keys at the top level of the args map. +;; This event is used to send messages to multiple recipients when you can't send it on a topic. +;; It is used for renewing keys in a private group chat, because if someone leaves/join. +;; We want to change the symmetric key but we can't do that in the group topic with the old key +;; otherwise leavers can still eavesdrop / joiners can read past history." +(re-frame/reg-fx + :shh/multi-post + (fn [{:keys [web3 message recipients success-event error-event] + :or {error-event :protocol/send-status-message-error}}] + (let [whisper-message (update message :payload (comp transport.utils/from-utf8 + transit/serialize))] + (doseq [{:keys [sym-key-id topic]} recipients] + (post-message {:web3 web3 + :whisper-message (assoc whisper-message + :symKeyID sym-key-id + :topic topic) + :on-success (if success-event + #(re-frame/dispatch success-event) + #(log/debug :shh/post-success)) + :on-error #(re-frame/dispatch [error-event %])}))))) + +(defn add-sym-key + [{:keys [web3 sym-key on-success on-error]}] + (.. web3 + -shh + (addSymKey sym-key (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))))) + +(defn get-sym-key + [{:keys [web3 sym-key-id on-success on-error]}] + (.. web3 + -shh + (getSymKey sym-key-id (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))))) + +(defn new-sym-key + [{:keys [web3 on-success on-error]}] + (.. web3 + -shh + (newSymKey (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))))) + +(defn log-error [error] + (log/error :shh/get-new-sym-key-error error)) + + +;;TODO (yenda) remove once go implements persistence +(re-frame/reg-fx + :shh/restore-sym-keys + (fn [{:keys [web3 transport on-success]}] + (doseq [[chat-id {:keys [sym-key]}] transport] + (add-sym-key {:web3 web3 + :sym-key sym-key + :on-success (fn [sym-key-id] + (on-success chat-id sym-key sym-key-id)) + :on-error log-error})))) + +(re-frame/reg-fx + :shh/add-new-sym-key + (fn [{:keys [web3 sym-key on-success]}] + (add-sym-key {:web3 web3 + :sym-key sym-key + :on-success (fn [sym-key-id] + (on-success sym-key sym-key-id)) + :on-error log-error}))) + +(re-frame/reg-fx + :shh/get-new-sym-key + (fn [{:keys [web3 on-success]}] + (new-sym-key {:web3 web3 + :on-success (fn [sym-key-id] + (get-sym-key {:web3 web3 + :sym-key-id sym-key-id + :on-success (fn [sym-key] + (on-success sym-key sym-key-id)) + :on-error log-error})) + :on-error log-error}))) + +(re-frame/reg-fx + :shh/generate-sym-key-from-password + (fn [{:keys [web3 password on-success]}] + (generate-sym-key-from-password {:web3 web3 + :password password + :on-success (fn [sym-key-id] + (get-sym-key {:web3 web3 + :sym-key-id sym-key-id + :on-success (fn [sym-key] + (on-success sym-key sym-key-id)) + :on-error log-error})) + :on-error log-error}))) diff --git a/src/status_im/protocol/web3/utils.cljs b/src/status_im/transport/utils.cljs similarity index 64% rename from src/status_im/protocol/web3/utils.cljs rename to src/status_im/transport/utils.cljs index 14828720c5..4d57a55cac 100644 --- a/src/status_im/protocol/web3/utils.cljs +++ b/src/status_im/transport/utils.cljs @@ -1,4 +1,5 @@ -(ns status-im.protocol.web3.utils +(ns ^{:doc "Utils for transport layer"} + status-im.transport.utils (:require [cljs-time.coerce :refer [to-long]] [cljs-time.core :refer [now]] [clojure.string :as string] @@ -13,6 +14,19 @@ (defn to-utf8 [s] (.toUtf8 dependencies/Web3.prototype (str s))) +(defn sha3 [s] + (.sha3 dependencies/Web3.prototype s)) + +(defn message-id + "Get a message-id" + [message] + (sha3 (pr-str message))) + +(defn get-topic + "Get the topic of a group chat or public chat from the chat-id" + [chat-id] + (subs (sha3 chat-id) 0 10)) + (defn shh [web3] (.-shh web3)) diff --git a/src/status_im/ui/components/list/views.cljs b/src/status_im/ui/components/list/views.cljs index 1f8e452a68..a31b0e2ead 100644 --- a/src/status_im/ui/components/list/views.cljs +++ b/src/status_im/ui/components/list/views.cljs @@ -102,7 +102,7 @@ (defn- wrap-key-fn [f] (fn [data index] - {:post [(string? %)]} + {:post [(some? %)]} (f data index))) (def default-separator [react/view styles/separator]) diff --git a/src/status_im/ui/screens/accounts/events.cljs b/src/status_im/ui/screens/accounts/events.cljs index e799809e4b..bfbac96b76 100644 --- a/src/status_im/ui/screens/accounts/events.cljs +++ b/src/status_im/ui/screens/accounts/events.cljs @@ -1,8 +1,6 @@ (ns status-im.ui.screens.accounts.events - (:require [status-im.data-store.accounts :as accounts-store] - [re-frame.core :as re-frame] + (:require [re-frame.core :as re-frame] [taoensso.timbre :as log] - [status-im.protocol.core :as protocol] [status-im.native-module.core :as status] [status-im.utils.types :refer [json->clj]] [status-im.utils.identicon :refer [identicon]] @@ -15,20 +13,13 @@ [status-im.utils.gfycat.core :refer [generate-gfy]] [status-im.utils.hex :as utils.hex] [status-im.constants :as constants] - status-im.ui.screens.accounts.create.navigation)) + [status-im.transport.message.v1.contact :as message.contact] + [status-im.transport.message.core :as transport] + status-im.ui.screens.accounts.create.navigation + [status-im.chat.models :as chat.models])) ;;;; COFX -(re-frame/reg-cofx - :get-new-keypair! - (fn [coeffects _] - (assoc coeffects :keypair (protocol/new-keypair!)))) - -(re-frame/reg-cofx - ::get-all-accounts - (fn [coeffects _] - (assoc coeffects :all-accounts (accounts-store/get-all)))) - (re-frame/reg-cofx ::get-signing-phrase (fn [coeffects _] @@ -41,11 +32,6 @@ ;;;; FX -(re-frame/reg-fx - ::save-account - (fn [account] - (accounts-store/save account true))) - (re-frame/reg-fx ::create-account (fn [password] @@ -53,33 +39,6 @@ password #(re-frame/dispatch [::account-created (json->clj %) password])))) -(re-frame/reg-fx - ::broadcast-account-update - (fn [{:keys [current-public-key web3 name photo-path status - updates-public-key updates-private-key]}] - (when web3 - (protocol/broadcast-profile! - {:web3 web3 - :message {:from current-public-key - :message-id (random/id) - :keypair {:public updates-public-key - :private updates-private-key} - :payload {:profile {:name name - :status status - :profile-image photo-path}}}})))) - -(re-frame/reg-fx - ::send-keys-update - (fn [{:keys [web3 current-public-key contacts - updates-public-key updates-private-key]}] - (doseq [id (handlers/identities contacts)] - (protocol/update-keys! - {:web3 web3 - :message {:from current-public-key - :to id - :message-id (random/id) - :payload {:keypair {:public updates-public-key - :private updates-private-key}}}})))) ;;;; Handlers (handlers/register-handler-fx @@ -96,8 +55,8 @@ :networks networks :wnode wnode :address address)] - {:db (assoc-in db [:accounts/accounts address] enriched-account) - ::save-account enriched-account})) + {:db (assoc-in db [:accounts/accounts address] enriched-account) + :data-store/save-account enriched-account})) ;; TODO(janherich) we have this handler here only because of the tests, refactor/improve tests ASAP (handlers/register-handler-fx @@ -107,17 +66,16 @@ (handlers/register-handler-fx ::account-created - [re-frame/trim-v (re-frame/inject-cofx :get-new-keypair!) - (re-frame/inject-cofx ::get-signing-phrase) (re-frame/inject-cofx ::get-status)] - (fn [{:keys [keypair signing-phrase status db]} [{:keys [pubkey address mnemonic]} password]] + [re-frame/trim-v (re-frame/inject-cofx ::get-signing-phrase) (re-frame/inject-cofx ::get-status)] + (fn [{:keys [signing-phrase status db] :as cofx} [{:keys [pubkey address mnemonic]} password]] (let [normalized-address (utils.hex/normalize-hex address) account {:public-key pubkey :address normalized-address :name (generate-gfy pubkey) :status status :signed-up? true - :updates-public-key (:public keypair) - :updates-private-key (:private keypair) + :updates-public-key "public" + :updates-private-key "private" :photo-path (identicon pubkey) :signing-phrase signing-phrase :mnemonic mnemonic @@ -129,7 +87,7 @@ (handlers/register-handler-fx :load-accounts - [(re-frame/inject-cofx ::get-all-accounts)] + [(re-frame/inject-cofx :data-store/get-all-accounts)] (fn [{:keys [db all-accounts]} _] (let [accounts (->> all-accounts (map (fn [{:keys [address] :as account}] @@ -147,66 +105,48 @@ (fn [{{:accounts/keys [accounts] :networks/keys [networks] :as db} :db} [_ id]] (let [current-account (get accounts id) new-account (assoc current-account :networks networks)] - {:db (assoc-in db [:accounts/accounts id] new-account) - ::save-account new-account}))) + {:db (assoc-in db [:accounts/accounts id] new-account) + :data-store/save-account new-account}))) (defn update-wallet-settings [{:accounts/keys [current-account-id accounts] :as db} settings] (let [new-account (-> (get accounts current-account-id) (assoc :settings settings))] - {:db (assoc-in db [:accounts/accounts current-account-id] new-account) - ::save-account new-account})) + {:db (assoc-in db [:accounts/accounts current-account-id] new-account) + :data-store/save-account new-account})) (defn account-update - "Takes effects (containing :db) + new account fields, adds all effects necessary for account update." - [{{:accounts/keys [accounts current-account-id] :as db} :db :as fx} new-account-fields] - (let [current-account (get accounts current-account-id) - new-account (merge current-account new-account-fields) - broadcast-fields [:name :photo-path :status - :updates-public-key :updates-private-key]] - (cond-> fx - true - (assoc-in [:db :accounts/accounts current-account-id] new-account) - true - (assoc ::save-account new-account) - - (seq (clojure.set/intersection (set (keys new-account-fields)) (set broadcast-fields))) - (assoc ::broadcast-account-update (merge (select-keys db [:current-public-key :web3]) - (select-keys new-account broadcast-fields)))))) - -(handlers/register-handler-fx - :account-update-keys - [(re-frame/inject-cofx :get-new-keypair!)] - (fn [{:keys [db keypair now]} _] - (let [{:accounts/keys [accounts current-account-id]} db - {:keys [public private]} keypair - current-account (get accounts current-account-id) - new-account (merge current-account {:updates-public-key public - :updates-private-key private - :last-updated now})] - {:db (assoc-in db [:accounts/accounts current-account-id] new-account) - ::save-account new-account - ::send-keys-update (merge - (select-keys db [:web3 :current-public-key :contacts]) - (select-keys new-account [:updates-public-key - :updates-private-key]))}))) + "Takes effects (containing :db) + new account fields, adds all effects necessary for account update. + Optionally, one can specify event to be dispatched after fields are persisted." + ([new-account-fields cofx] + (account-update new-account-fields nil cofx)) + ([new-account-fields after-update-event {{:accounts/keys [accounts current-account-id] :as db} :db :as cofx}] + (let [current-account (get accounts current-account-id) + new-account (merge current-account new-account-fields) + fx {:db (assoc-in db [:accounts/accounts current-account-id] new-account) + :data-store/save-account (assoc new-account :after-update-event after-update-event)} + {:keys [name photo-path]} new-account] + (if (or (:name new-account-fields) (:photo-path new-account-fields)) + (handlers/merge-fx cofx fx (transport/send (message.contact/ContactUpdate. name photo-path) nil)) + fx)))) (handlers/register-handler-fx :send-account-update-if-needed - (fn [{{:accounts/keys [accounts current-account-id] :as db} :db now :now} _] + (fn [{{:accounts/keys [accounts current-account-id] :as db} :db now :now :as cofx} _] (let [{:keys [last-updated]} (get accounts current-account-id) - needs-update? (> (- now last-updated) time/week)] + needs-update? (> (- now last-updated) time/week)] (log/info "Need to send account-update: " needs-update?) (when needs-update? ;; TODO(janherich): this is very strange and misleading, need to figure out why it'd necessary to update ;; account with network update when last update was more then week ago - (account-update {:db db} nil))))) + (account-update nil cofx))))) (handlers/register-handler-fx :account-set-name - (fn [{{:accounts/keys [create] :as db} :db} _] - (-> {:db (assoc-in db [:accounts/create :show-welcome?] true) - :dispatch [:navigate-to-clean :usage-data [:account-finalized]]} - (account-update {:name (:name create)})))) + (fn [{{:accounts/keys [create] :as db} :db :as cofx} _] + (handlers/merge-fx cofx + {:db (assoc-in db [:accounts/create :show-welcome?] true) + :dispatch [:navigate-to-clean :usage-data [:account-finalized]]} + (account-update {:name (:name create)})))) (handlers/register-handler-fx :account-finalized @@ -222,8 +162,8 @@ (handlers/register-handler-fx :update-sign-in-time - (fn [{db :db now :now} _] - (account-update {:db db} {:last-sign-in now}))) + (fn [{db :db now :now :as cofx} _] + (account-update {:last-sign-in now} cofx))) (handlers/register-handler-fx :reset-account-creation @@ -232,5 +172,5 @@ (handlers/register-handler-fx :switch-dev-mode - (fn [{db :db} [_ dev-mode]] - (account-update {:db db} {:dev-mode? dev-mode}))) + (fn [cofx [_ dev-mode]] + (account-update {:dev-mode? dev-mode} cofx))) diff --git a/src/status_im/ui/screens/accounts/recover/events.cljs b/src/status_im/ui/screens/accounts/recover/events.cljs index 80680b938c..e1a2ad5c0c 100644 --- a/src/status_im/ui/screens/accounts/recover/events.cljs +++ b/src/status_im/ui/screens/accounts/recover/events.cljs @@ -27,7 +27,6 @@ (handlers/register-handler-fx :account-recovered - [(re-frame/inject-cofx :get-new-keypair!)] (fn [{:keys [db keypair]} [_ result password]] (let [data (types/json->clj result) public-key (:pubkey data) @@ -38,14 +37,15 @@ :address address :name (gfycat/generate-gfy public-key) :photo-path (identicon/identicon public-key) - :updates-public-key public - :updates-private-key private + ;;TODO remove those + :updates-public-key "public" + :updates-private-key "private" :mnemonic "" :signed-up? true :signing-phrase phrase :settings constants/default-account-settings}] (when-not (string/blank? public-key) - (-> db + (-> db (accounts-events/add-account account) (assoc :dispatch [:login-account address password]) (assoc :dispatch-later [{:ms 2000 :dispatch [:navigate-to :usage-data]}])))))) diff --git a/src/status_im/ui/screens/add_new/views.cljs b/src/status_im/ui/screens/add_new/views.cljs index e22ee06076..cf2f337e54 100644 --- a/src/status_im/ui/screens/add_new/views.cljs +++ b/src/status_im/ui/screens/add_new/views.cljs @@ -21,12 +21,13 @@ :icon-opts {:color colors/blue} :on-press #(re-frame/dispatch [:navigate-to :new-chat])}] [action-button/action-separator] - [action-button/action-button - {:label (i18n/label :t/start-group-chat) - :accessibility-label :start-group-chat-button - :icon :icons/contacts - :icon-opts {:color colors/blue} - :on-press #(re-frame/dispatch [:open-contact-toggle-list :chat-group])}] + ;; TODO temporary removal before everything is fixed in group chats + #_[action-button/action-button + {:label (i18n/label :t/start-group-chat) + :accessibility-label :start-group-chat-button + :icon :icons/contacts + :icon-opts {:color colors/blue} + :on-press #(re-frame/dispatch [:open-contact-toggle-list :chat-group])}] [action-button/action-separator] [action-button/action-button {:label (i18n/label :t/new-public-group-chat) diff --git a/src/status_im/ui/screens/browser/events.cljs b/src/status_im/ui/screens/browser/events.cljs index 314e480e90..03530353fa 100644 --- a/src/status_im/ui/screens/browser/events.cljs +++ b/src/status_im/ui/screens/browser/events.cljs @@ -1,33 +1,17 @@ (ns status-im.ui.screens.browser.events (:require status-im.ui.screens.browser.navigation [status-im.utils.handlers :as handlers] - [status-im.data-store.browser :as browser-store] [re-frame.core :as re-frame] [status-im.utils.random :as random] [status-im.i18n :as i18n])) -(re-frame/reg-cofx - :all-stored-browsers - (fn [cofx _] - (assoc cofx :all-stored-browsers (browser-store/get-all)))) - (handlers/register-handler-fx :initialize-browsers - [(re-frame/inject-cofx :all-stored-browsers)] + [(re-frame/inject-cofx :data-store/all-browsers)] (fn [{:keys [db all-stored-browsers]} _] (let [browsers (into {} (map #(vector (:browser-id %) %) all-stored-browsers))] {:db (assoc db :browser/browsers browsers)}))) -(re-frame/reg-fx - :save-browser - (fn [browser] - (browser-store/save browser))) - -(re-frame/reg-fx - :remove-browser - (fn [browser-id] - (browser-store/delete browser-id))) - (defn match-url [url] (str (when (and url (not (re-find #"^[a-zA-Z-_]+:/" url))) "http://") url)) diff --git a/src/status_im/ui/screens/contacts/core.cljs b/src/status_im/ui/screens/contacts/core.cljs new file mode 100644 index 0000000000..3b07526421 --- /dev/null +++ b/src/status_im/ui/screens/contacts/core.cljs @@ -0,0 +1,64 @@ +(ns status-im.ui.screens.contacts.core + (:require [re-frame.core :as re-frame] [status-im.utils.handlers :as handlers] + [status-im.data-store.messages :as data-store.messages] + [status-im.chat.models :as chat.models] + [status-im.constants :as constants])) + +(defn receive-contact-request + [public-key + {:keys [name profile-image address fcm-token]} + {{:contacts/keys [contacts] :as db} :db :as cofx}] + (when-not (get contacts public-key) + (let [contact-props {:whisper-identity public-key + :public-key public-key + :address address + :photo-path profile-image + :name name + :fcm-token fcm-token + :pending? true} + chat-props {:name name + :chat-id public-key + :contact-info (prn-str contact-props)}] + (handlers/merge-fx cofx + {:db (update-in db [:contacts/contacts public-key] merge contact-props) + :data-store/save-contact contact-props} + (chat.models/add-chat public-key chat-props))))) + +(defn receive-contact-request-confirmation + [public-key {:keys [name profile-image address fcm-token]} + {{:contacts/keys [contacts] :as db} :db :as cofx}] + (when-let [contact (get contacts public-key)] + (let [contact-props {:whisper-identity public-key + :public-key public-key + :address address + :photo-path profile-image + :name name + :fcm-token fcm-token} + chat-props {:name name + :chat-id public-key}] + (handlers/merge-fx cofx + {:db (update-in db [:contacts/contacts public-key] merge contact-props) + :data-store/save-contact contact-props} + (chat.models/upsert-chat chat-props))))) + + +(defn- update-contact [{:keys [whisper-identity] :as contact} {:keys [db]}] + (when (get-in db [:contacts/contacts whisper-identity]) + {:db (update-in db [:contacts/contacts whisper-identity] merge contact) + :data-store/save-contact contact})) + +(defn receive-contact-update [chat-id public-key {:keys [name profile-image]} {:keys [db now] :as cofx}] + (let [{:keys [chats current-public-key]} db] + (when (not= current-public-key public-key) + (let [prev-last-updated (get-in db [:contacts/contacts public-key :last-updated])] + (when (<= prev-last-updated now) + (let [contact {:whisper-identity public-key + :name name + :photo-path profile-image + :last-updated now}] + (if (chats public-key) + (handlers/merge-fx cofx + (update-contact contact) + (chat.models/update-chat {:chat-id chat-id + :name name})) + (update-contact contact cofx)))))))) diff --git a/src/status_im/ui/screens/contacts/db.cljs b/src/status_im/ui/screens/contacts/db.cljs index 6c3a11e30b..84fcc17230 100644 --- a/src/status_im/ui/screens/contacts/db.cljs +++ b/src/status_im/ui/screens/contacts/db.cljs @@ -28,7 +28,7 @@ (spec/def :contact/dapp? boolean?) (spec/def :contact/dapp-url (spec/nilable string?)) (spec/def :contact/dapp-hash (spec/nilable int?)) -(spec/def :contact/bot-url (spec/nilable string?)) +(spec/def :contact/bot-url (spec/nilable string?)) (spec/def :contact/command (spec/nilable (spec/map-of int? map?))) (spec/def :contact/response (spec/nilable (spec/map-of int? map?))) (spec/def :contact/jail-loaded? (spec/nilable boolean?)) @@ -48,13 +48,13 @@ :contact/status :contact/last-updated :contact/last-online - :contact/pending? - :contact/hide-contact? + :contact/pending? + :contact/hide-contact? :contact/unremovable? :contact/dapp? :contact/dapp-url :contact/dapp-hash - :contact/bot-url + :contact/bot-url :contact/jail-loaded? :contact/jail-loaded-events :contact/command @@ -84,4 +84,4 @@ ;used in modal list (for example for wallet) (spec/def :contacts/click-action (spec/nilable #{:send :request})) ;used in modal list (for example for wallet) -(spec/def :contacts/click-params (spec/nilable map?)) \ No newline at end of file +(spec/def :contacts/click-params (spec/nilable map?)) diff --git a/src/status_im/ui/screens/contacts/events.cljs b/src/status_im/ui/screens/contacts/events.cljs index bf7762f1c8..8ac65dac68 100644 --- a/src/status_im/ui/screens/contacts/events.cljs +++ b/src/status_im/ui/screens/contacts/events.cljs @@ -1,120 +1,42 @@ (ns status-im.ui.screens.contacts.events - (:require [re-frame.core :refer [dispatch trim-v reg-fx reg-cofx inject-cofx]] - [status-im.utils.handlers :refer [register-handler-db register-handler-fx]] - [status-im.data-store.contacts :as contacts] - [clojure.string :as s] - [status-im.protocol.core :as protocol] + (:require [cljs.reader :as reader] + [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] [status-im.utils.contacts :as utils.contacts] [status-im.utils.random :as random] - [cljs.reader :refer [read-string]] [status-im.utils.js-resources :as js-res] [status-im.i18n :refer [label]] + [taoensso.timbre :as log] + [status-im.utils.js-resources :as js-res] + [status-im.utils.datetime :as datetime] + [status-im.utils.identicon :as identicon] + [status-im.utils.gfycat.core :as gfycat.core] [status-im.ui.screens.contacts.navigation] [status-im.ui.screens.navigation :as navigation] [status-im.chat.console :as console-chat] + [status-im.chat.events :as chat.events] + [status-im.chat.models :as chat.models] [status-im.commands.events.loading :as loading-events] - [status-im.ui.screens.add-new.new-chat.db :as new-chat.db] - [status-im.utils.datetime :as datetime] - [status-im.js-dependencies :as js-dependencies])) + [status-im.js-dependencies :as js-dependencies] + [status-im.transport.message.core :as transport] + [status-im.transport.message.v1.contact :as message.v1.contact] + [status-im.ui.screens.add-new.new-chat.db :as new-chat.db])) ;;;; COFX -(reg-cofx - :get-all-contacts +(re-frame/reg-cofx + ::get-default-contacts-and-groups (fn [coeffects _] - (assoc coeffects :all-contacts (contacts/get-all)))) - -(reg-cofx - ::get-default-contacts-and-groups - (fn [coeffects _] - (assoc coeffects - :default-contacts js-res/default-contacts - :default-groups js-res/default-contact-groups))) - -;;;; FX - -(reg-fx - ::watch-contact - (fn [{:keys [web3 whisper-identity public-key private-key]}] - (protocol/watch-user! {:web3 web3 - :identity whisper-identity - :keypair {:public public-key - :private private-key} - :callback #(dispatch [:incoming-message %1 %2])}))) - -(reg-fx - ::stop-watching-contact - (fn [{:keys [web3 whisper-identity]}] - (protocol/stop-watching-user! {:web3 web3 - :identity whisper-identity}))) - -(reg-fx - ::send-contact-request-fx - (fn [{:keys [web3 current-public-key name whisper-identity - photo-path current-account-id status fcm-token - updates-public-key updates-private-key] :as params}] - (protocol/contact-request! - {:web3 web3 - :message {:from current-public-key - :to whisper-identity - :message-id (random/id) - :payload {:contact {:name name - :profile-image photo-path - :address current-account-id - :status status - :fcm-token fcm-token} - :keypair {:public updates-public-key - :private updates-private-key} - :timestamp (datetime/timestamp)}}}))) - -(reg-fx - ::reset-pending-messages - (fn [from] - (protocol/reset-pending-messages! from))) - -(reg-fx - ::save-contact - (fn [contact] - (contacts/save contact))) - -(reg-fx - :save-all-contacts - (fn [new-contacts] - (contacts/save-all new-contacts))) - -(reg-fx - ::delete-contact - (fn [contact] - (contacts/delete contact))) - -(defn- contact-name [contact] - (->> contact - ((juxt :givenName :middleName :familyName)) - (remove s/blank?) - (s/join " "))) + (assoc coeffects + :default-contacts js-res/default-contacts + :default-groups js-res/default-contact-groups))) ;;;; Handlers -(defn watch-contact - "Takes effects map, adds effects necessary to start watching contact" - [{:keys [db] :as fx} {:keys [public-key private-key] :as contact}] - (cond-> fx - (and public-key private-key) - (assoc ::watch-contact (merge - (select-keys db [:web3]) - (select-keys contact [:whisper-identity :public-key :private-key]))))) - -(register-handler-fx - :watch-contact - (fn [{:keys [db]} [_ contact]] - (watch-contact {:db db} contact))) - -(register-handler-fx - :update-contact! - (fn [{:keys [db]} [_ {:keys [whisper-identity] :as contact}]] - (when (get-in db [:contacts/contacts whisper-identity]) - {:db (update-in db [:contacts/contacts whisper-identity] merge contact) - ::save-contact contact}))) +(defn- update-contact [{:keys [whisper-identity] :as contact} {:keys [db]}] + (when (get-in db [:contacts/contacts whisper-identity]) + {:db (update-in db [:contacts/contacts whisper-identity] merge contact) + :data-store/save-contact contact})) (defn- update-pending-status [old-contacts {:keys [whisper-identity pending?] :as contact}] (let [{old-pending :pending? @@ -183,9 +105,9 @@ (:group-id (first contacts)) (mapv :whisper-identity contacts)]))) -(register-handler-fx +(handlers/register-handler-fx :load-default-contacts! - [(inject-cofx ::get-default-contacts-and-groups)] + [(re-frame/inject-cofx ::get-default-contacts-and-groups)] (fn [{:keys [db default-contacts default-groups]} _] (let [{:contacts/keys [contacts] :group/keys [contact-groups]} db] {:dispatch-n (concat @@ -194,21 +116,17 @@ (prepare-add-chat-events contacts default-contacts) (prepare-add-contacts-to-groups-events contacts default-contacts))}))) -(register-handler-fx +(handlers/register-handler-fx :load-contacts - [(inject-cofx :get-all-contacts)] + [(re-frame/inject-cofx :data-store/get-all-contacts)] (fn [{:keys [db all-contacts]} _] (let [contacts-list (map #(vector (:whisper-identity %) %) all-contacts) contacts (into {} contacts-list)] - {:db (update db :contacts/contacts #(merge contacts %))}))) - ;; TODO (yenda) this mapv was dispatching useless events, fixed but is it necessary if - ;; it was dispatching useless events before with nil - ;;:dispatch-n (mapv (fn [[_ contact]] [:watch-contact contact]) contacts) + {:db (update db :contacts/contacts #(merge contacts %))}))) - -(register-handler-fx +(handlers/register-handler-fx :add-contacts - [(inject-cofx :get-local-storage-data)] + [(re-frame/inject-cofx :data-store/get-local-storage-data)] (fn [{:keys [db] :as cofx} [_ new-contacts]] (let [{:contacts/keys [contacts]} db new-contacts' (->> new-contacts @@ -217,158 +135,107 @@ ;;(remove #(identities (:whisper-identity %))) (map #(vector (:whisper-identity %) %)) (into {})) - fx {:db (update db :contacts/contacts merge new-contacts') - :save-all-contacts (vals new-contacts')}] + fx {:db (update db :contacts/contacts merge new-contacts') + :data-store/save-contacts (vals new-contacts')}] (transduce (map second) (completing (partial loading-events/load-commands (assoc cofx :db (:db fx)))) fx new-contacts')))) -(register-handler-db - :remove-contacts-click-handler - (fn [db _] - (dissoc db - :contacts/click-handler - :contacts/click-action))) +(defn- add-new-contact [{:keys [whisper-identity] :as contact} {:keys [db]}] + (let [new-contact (assoc contact :pending? false)] + {:db (-> db + (update-in [:contacts/contacts whisper-identity] merge new-contact) + (assoc-in [:contacts/new-identity] "")) + :data-store/save-contact new-contact})) -(defn send-contact-request - "Takes effects map, adds effects necessary to send a contact request" - [{{:accounts/keys [accounts current-account-id] :as db} :db :as fx} contact] - (let [current-account (get accounts current-account-id) +(defn- own-info [{:accounts/keys [accounts current-account-id] :as db}] + (let [{:keys [name photo-path address]} (get accounts current-account-id) fcm-token (get-in db [:notifications :fcm-token])] - (assoc fx - ::send-contact-request-fx - (merge - (select-keys db [:current-public-key :web3]) - {:current-account-id current-account-id :fcm-token fcm-token} - (select-keys contact [:whisper-identity]) - (select-keys current-account [:name :photo-path :status - :updates-public-key :updates-private-key]))))) + {:name name + :profile-image photo-path + :address address + :fcm-token fcm-token})) -(defn add-new-contact [fx {:keys [whisper-identity] :as contact}] - (-> fx - (send-contact-request contact) - (update-in [:db :contacts/contacts whisper-identity] merge contact) - (assoc-in [:db :contacts/new-identity] "") - (assoc ::save-contact contact))) +(defn send-contact-request [{:keys [whisper-identity pending? dapp?] :as contact} {:keys [db] :as cofx}] + (when-not dapp? + (if pending? + (transport/send (message.v1.contact/map->ContactRequestConfirmed (own-info db)) whisper-identity cofx) + (transport/send (message.v1.contact/map->ContactRequest (own-info db)) whisper-identity cofx)))) -(defn add-contact [{:keys [db] :as fx} chat-or-whisper-id] - (let [{:keys [chats] :contacts/keys [contacts]} db - contact (if-let [contact-info (get-in chats [chat-or-whisper-id :contact-info])] - (read-string contact-info) - (or (get contacts chat-or-whisper-id) - (utils.contacts/whisper-id->new-contact chat-or-whisper-id))) - contact' (assoc contact - :address (public-key->address chat-or-whisper-id) - :pending? false)] - (-> fx - (watch-contact contact') - (add-new-contact contact')))) +(defn- build-contact [whisper-id {{:keys [chats] :contacts/keys [contacts]} :db}] + (-> (if-let [contact-info (get-in chats [whisper-id :contact-info])] + (reader/read-string contact-info) + (or (get contacts whisper-id) + (utils.contacts/whisper-id->new-contact whisper-id))) + (assoc :address (public-key->address whisper-id)))) -(defn add-contact-and-open-chat [fx whisper-id] - (-> fx - (add-contact whisper-id) - (update :db #(navigation/navigate-to-clean % :home)) - (assoc :dispatch [:start-chat whisper-id {:navigation-replace? true}]))) +(defn add-contact [whisper-id {:keys [db] :as cofx}] + (let [contact (build-contact whisper-id cofx)] + (handlers/merge-fx cofx + (add-new-contact contact) + (send-contact-request contact)))) -(register-handler-fx +(defn add-contact-and-open-chat [whisper-id cofx] + (handlers/merge-fx cofx + (navigation/navigate-to-clean :home) + (add-contact whisper-id) + (chat.events/start-chat whisper-id {}))) + +(handlers/register-handler-fx :add-contact - (fn [{:keys [db]} [_ chat-or-whisper-id]] - (add-contact {:db db} chat-or-whisper-id))) + [(re-frame/inject-cofx :random-id)] + (fn [cofx [_ whisper-id]] + (add-contact whisper-id cofx))) -(register-handler-fx - :add-contact-and-open-chat - (fn [{:keys [db]} [_ whisper-id]] - (add-contact-and-open-chat {:db db} whisper-id))) - -(register-handler-fx +(handlers/register-handler-fx :set-contact-identity-from-qr - (fn [{{:accounts/keys [accounts current-account-id] :as db} :db} [_ _ contact-identity]] - (let [current-account (get accounts current-account-id)] - (cond-> {:db (assoc db :contacts/new-identity contact-identity)} - (not (new-chat.db/validate-pub-key contact-identity current-account)) - (assoc :dispatch [:add-contact-handler]))))) + [(re-frame/inject-cofx :random-id) (re-frame/inject-cofx :data-store/get-chat)] + (fn [{{:accounts/keys [accounts current-account-id] :as db} :db :as cofx} [_ _ contact-identity]] + (let [current-account (get accounts current-account-id) + fx {:db (assoc db :contacts/new-identity contact-identity)}] + (if (new-chat.db/validate-pub-key contact-identity current-account) + fx + (handlers/merge-fx cofx + fx + (add-contact-and-open-chat contact-identity)))))) -(register-handler-fx - :contact-update-received - (fn [{:keys [db]} [_ {:keys [from payload]}]] - (let [{:keys [chats current-public-key]} db] - (when (not= current-public-key from) - (let [{:keys [content timestamp]} payload - {:keys [status name profile-image]} (:profile content) - prev-last-updated (get-in db [:contacts/contacts from :last-updated])] - (when (<= prev-last-updated timestamp) - (let [contact {:whisper-identity from - :name name - :photo-path profile-image - :status status - :last-updated timestamp}] - {:dispatch-n (concat [[:update-contact! contact]] - (when (chats from) - [[:update-chat! {:chat-id from - :name name}]]))}))))))) - -(register-handler-fx - :update-keys-received - (fn [{:keys [db]} [_ {:keys [from payload]}]] - (let [{{:keys [public private]} :keypair - timestamp :timestamp} payload - prev-last-updated (get-in db [:contacts/contacts from :keys-last-updated])] - (when (<= prev-last-updated timestamp) - (let [contact {:whisper-identity from - :public-key public - :private-key private - :keys-last-updated timestamp}] - {:dispatch [:update-contact! contact]}))))) - -(register-handler-fx - :contact-online-received - (fn [{:keys [db]} [_ {:keys [from] - {{:keys [timestamp]} :content} :payload}]] - (let [prev-last-online (get-in db [:contacts/contacts from :last-online])] - (when (and timestamp (< prev-last-online timestamp)) - {::reset-pending-messages from - :dispatch [:update-contact! {:whisper-identity from - :last-online timestamp}]})))) - -(register-handler-fx +(handlers/register-handler-fx :hide-contact - (fn [{:keys [db]} [_ {:keys [whisper-identity] :as contact}]] - {::stop-watching-contact (merge - (select-keys db [:web3]) - (select-keys contact [:whisper-identity])) - :dispatch-n [[:update-contact! {:whisper-identity whisper-identity - :pending? true}] - [:account-update-keys]]})) + (fn [cofx [_ {:keys [whisper-identity] :as contact}]] + (update-contact {:whisper-identity whisper-identity + :pending? true} + cofx))) ;;used only by status-dev-cli -(register-handler-fx +(handlers/register-handler-fx :remove-contact - (fn [{:keys [db]} [_ whisper-identity pred]] - (let [contact (get-in db [:contacts/contacts whisper-identity])] - (when (and contact (pred contact)) - {:db (update db :contacts/contacts dissoc whisper-identity) - ::delete-contact contact})))) + (fn [{:keys [db]} [_ whisper-identity]] + (when-let [contact (get-in db [:contacts/contacts whisper-identity])] + {:db (update db :contacts/contacts dissoc whisper-identity) + :data-store/delete-contact contact}))) -(register-handler-fx +(handlers/register-handler-db :open-contact-toggle-list - (fn [{:keys [db]} [_ group-type]] - {:db (-> db - (assoc :group/group-type group-type - :group/selected-contacts #{} - :new-chat-name "") - (navigation/navigate-to :contact-toggle-list))})) + (fn [db [_ group-type]] + (-> (assoc db + :group/group-type group-type + :group/selected-contacts #{} + :new-chat-name "") + (navigation/navigate-to :contact-toggle-list)))) -(register-handler-fx +(handlers/register-handler-fx :open-chat-with-contact - (fn [{:keys [db]} [_ {:keys [whisper-identity dapp?] :as contact}]] - (-> {:db db} - (cond-> (when-not dapp?) (send-contact-request contact)) - (update :db #(navigation/navigate-to-clean % :home)) - (assoc :dispatch [:start-chat whisper-identity])))) + [(re-frame/inject-cofx :random-id)] + (fn [{:keys [db] :as cofx} [_ {:keys [whisper-identity] :as contact}]] + (handlers/merge-fx cofx + (navigation/navigate-to-clean :home) + (add-contact whisper-identity) + (chat.events/start-chat whisper-identity {})))) -(register-handler-fx +(handlers/register-handler-fx :add-contact-handler - (fn [{{:contacts/keys [new-identity] :as db} :db}] + [(re-frame/inject-cofx :random-id) (re-frame/inject-cofx :data-store/get-chat)] + (fn [{{:contacts/keys [new-identity] :as db} :db :as cofx} _] (when (seq new-identity) - (add-contact-and-open-chat {:db db} new-identity)))) + (add-contact-and-open-chat new-identity cofx)))) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index c52e1ff842..3120948c98 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -3,6 +3,7 @@ (:require [cljs.spec.alpha :as spec] [status-im.constants :as constants] [status-im.utils.platform :as platform] + status-im.transport.db status-im.ui.screens.accounts.db status-im.ui.screens.contacts.db status-im.ui.screens.qr-scanner.db @@ -45,6 +46,7 @@ :inbox/topic constants/inbox-topic :inbox/password constants/inbox-password :my-profile/editing? false + :transport/chats {} :desktop/desktop {:tab-view-id :home}}) ;;;;GLOBAL @@ -168,6 +170,8 @@ :browser/options :new/open-dapp :navigation/screen-params + :transport/chats + :transport/discovery-filter :desktop/desktop] :opt-un [::current-public-key diff --git a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs b/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs index a6fbf29a05..2a748f9764 100644 --- a/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs +++ b/src/status_im/ui/screens/desktop/main/tabs/profile/views.cljs @@ -1,6 +1,7 @@ (ns status-im.ui.screens.desktop.main.tabs.profile.views (:require-macros [status-im.utils.views :as views]) - (:require [status-im.ui.components.react :as react] + (:require [re-frame.core :as re-frame] + [status-im.ui.components.react :as react] [status-im.ui.screens.profile.user.views :as profile])) (defn profile-badge [{:keys [name]}] @@ -34,7 +35,7 @@ [react/view [my-profile-info current-account]] [react/view {:style {:height 1 :background-color "#e8ebec" :margin-horizontal 16}}] - [react/touchable-highlight {:on-press #(profile/navigate-to-accounts false) + [react/touchable-highlight {:on-press #(re-frame/dispatch [:logout]) :style {:margin-top 60}} [react/view [react/text {:style {:color :red}} "Log out"]]]])) diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index 139371e669..74ace874bc 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -1,10 +1,11 @@ (ns status-im.ui.screens.events (:require status-im.bots.events - status-im.chat.handlers + status-im.chat.events status-im.commands.handlers.jail status-im.commands.events.loading status-im.commands.handlers.debug status-im.network.events + status-im.transport.handlers status-im.protocol.handlers status-im.ui.screens.accounts.events status-im.ui.screens.accounts.login.events @@ -31,6 +32,7 @@ [status-im.data-store.core :as data-store] [status-im.i18n :as i18n] [status-im.js-dependencies :as dependencies] + [status-im.transport.core :as transport] [status-im.ui.screens.db :refer [app-db]] [status-im.utils.datetime :as time] [status-im.utils.ethereum.core :as ethereum] @@ -50,7 +52,7 @@ ;;;; Helper fns (defn- call-jail-function - [{:keys [chat-id function callback-events-creator] :as opts}] + [{:keys [chat-id function callback-event-creator] :as opts}] (let [path [:functions function] params (select-keys opts [:parameters :context])] (status/call-jail @@ -58,12 +60,11 @@ :path path :params params :callback (fn [jail-response] - (doseq [event (if callback-events-creator - (callback-events-creator jail-response) - [[:chat-received-message/bot-response - {:chat-id chat-id} - jail-response]]) - :when event] + (when-let [event (if callback-event-creator + (callback-event-creator jail-response) + [:chat-received-message/bot-response + {:chat-id chat-id} + jail-response])] (re-frame/dispatch event)))}))) ;;;; COFX @@ -89,15 +90,14 @@ (re-frame/reg-fx :call-jail - (fn [{:keys [callback-events-creator] :as opts}] + (fn [{:keys [callback-event-creator] :as opts}] (status/call-jail (-> opts - (dissoc :callback-events-creator) + (dissoc :callback-event-creator) (assoc :callback (fn [jail-response] - (when callback-events-creator - (doseq [event (callback-events-creator jail-response)] - (re-frame/dispatch event))))))))) + (when-let [event (callback-event-creator jail-response)] + (re-frame/dispatch event)))))))) (re-frame/reg-fx :call-jail-function @@ -168,7 +168,7 @@ (re-frame/reg-fx ::status-module-initialized-fx - (fn [] + (fn [_] (status/module-initialized!))) (re-frame/reg-fx @@ -183,7 +183,7 @@ (re-frame/reg-fx ::testfairy-alert - (fn [] + (fn [_] (when config/testfairy-enabled? (utils/show-popup (i18n/label :testfairy-title) @@ -191,7 +191,7 @@ (re-frame/reg-fx ::get-fcm-token-fx - (fn [] + (fn [_] (notifications/get-fcm-token))) (re-frame/reg-fx @@ -207,7 +207,8 @@ (re-frame/reg-fx :close-application - (fn [] (status/close-application))) + (fn [_] + (status/close-application))) ;;;; Handlers @@ -232,6 +233,20 @@ [:initialize-crypt] [:initialize-geth]]})) +(handlers/register-handler-fx + :logout + (fn [{:keys [db] :as cofx} _] + (let [{:transport/keys [chats] :keys [current-account-id]} db + sharing-usage-data? (get-in db [:accounts/accounts current-account-id :sharing-usage-data?])] + (handlers/merge-fx cofx + {:dispatch-n (concat [[:initialize-db] + [:load-accounts] + [:listen-to-network-status] + [:navigate-to :accounts]] + (when sharing-usage-data? + [[:unregister-mixpanel-tracking]]))} + (transport/stop-whisper))))) + (handlers/register-handler-fx :initialize-db (fn [{{:keys [status-module-initialized? status-node-started? @@ -280,7 +295,6 @@ :initialize-account (fn [_ [_ address events-after]] {:dispatch-n (cond-> [[:initialize-account-db address] - [:load-processed-messages] [:initialize-protocol address] [:initialize-sync-listener] [:initialize-chats] diff --git a/src/status_im/ui/screens/group/chat_settings/events.cljs b/src/status_im/ui/screens/group/chat_settings/events.cljs index 0c86baf541..20458f27dc 100644 --- a/src/status_im/ui/screens/group/chat_settings/events.cljs +++ b/src/status_im/ui/screens/group/chat_settings/events.cljs @@ -1,132 +1,11 @@ (ns status-im.ui.screens.group.chat-settings.events (:require [re-frame.core :as re-frame] - [status-im.constants :as constants] - [status-im.data-store.chats :as chats] - [status-im.data-store.contacts :as contacts] - [status-im.data-store.messages :as messages] [status-im.i18n :as i18n] - [status-im.protocol.core :as protocol] - [status-im.utils.handlers :as handlers] - [status-im.utils.random :as random])) + [status-im.chat.models.message :as models.message] + [status-im.transport.message.v1.group-chat :as group-chat] + [status-im.transport.message.core :as transport] + [status-im.utils.handlers :as handlers])) -;;;; FX - -(re-frame/reg-fx - ::save-chat-property - (fn [[current-chat-id property-name value]] - (chats/save-property current-chat-id property-name value))) - -(re-frame/reg-fx - ::add-members-to-chat - (fn [{:keys [current-chat-id selected-participants]}] - (chats/add-contacts current-chat-id selected-participants))) - -(re-frame/reg-fx - ::remove-members-from-chat - (fn [[current-chat-id participants]] - (chats/remove-contacts current-chat-id participants))) - -(defn system-message [message-id content] - {:from "system" - :message-id message-id - :content content - :content-type constants/text-content-type}) - -(defn removed-participant-message [chat-id identity contact-name] - (let [message-text (str "You've removed " (or contact-name identity))] - (-> (system-message (random/id) message-text) - (assoc :chat-id chat-id) - (messages/save)))) - -(re-frame/reg-fx - ::create-removing-messages - (fn [{:keys [current-chat-id participants contacts/contacts]}] - (doseq [participant participants] - (let [contact-name (get-in contacts [participant :name])] - (removed-participant-message current-chat-id participant contact-name))))) - -(re-frame/reg-fx - ::notify-about-new-members - (fn [{:keys [current-chat-id selected-participants - current-public-key chats web3]}] - (let [{:keys [name contacts]} (chats current-chat-id) - identities (map :identity contacts) - - {:keys [public private] - :as new-keypair} (protocol/new-keypair!) - - group-message {:web3 web3 - :group {:id current-chat-id - :name name - :contacts (conj identities current-public-key) - :admin current-public-key} - :message {:from current-public-key - :message-id (random/id)}}] - (re-frame/dispatch [:update-chat! {:chat-id current-chat-id - :public-key public - :private-key private}]) - (protocol/start-watching-group! {:web3 web3 - :group-id current-chat-id - :identity current-public-key - :keypair new-keypair - :callback #(re-frame/dispatch [:incoming-message %1 %2])}) - (protocol/invite-to-group! - (-> group-message - (assoc-in [:group :keypair] new-keypair) - (assoc :identities selected-participants))) - (protocol/update-group! - (-> group-message - (assoc-in [:group :keypair] new-keypair) - (assoc :identities identities))) - (doseq [identity selected-participants] - (protocol/add-to-group! {:web3 web3 - :group-id current-chat-id - :identity identity - :keypair new-keypair - :message {:from current-public-key - :message-id (random/id)}}))))) - -(re-frame/reg-fx - ::notify-about-removing - (fn [{:keys [web3 current-chat-id participants chats current-public-key]}] - (let [{:keys [private public] :as new-keypair} (protocol/new-keypair!) - {:keys [name private-key public-key] - :as chat} (get chats current-chat-id) - old-keypair {:private private-key - :public public-key} - contacts (get chat :contacts) - identities (-> (map :identity contacts) - set - (clojure.set/difference participants))] - (re-frame/dispatch [:update-chat! {:chat-id current-chat-id - :private-key private - :public-key public}]) - (doseq [participant participants] - (let [id (random/id)] - (doseq [keypair [old-keypair new-keypair]] - (protocol/remove-from-group! - {:web3 web3 - :group-id current-chat-id - :identity participant - :keypair keypair - :message {:from current-public-key - :message-id id}})))) - (protocol/start-watching-group! - {:web3 web3 - :group-id current-chat-id - :identity current-public-key - :keypair new-keypair - :callback #(re-frame/dispatch [:incoming-message %1 %2])}) - (protocol/update-group! - {:web3 web3 - :group {:id current-chat-id - :name name - :contacts (conj identities current-public-key) - :admin current-public-key - :keypair new-keypair} - :identities identities - :message {:from current-public-key - :message-id (random/id)}})))) ;;;; Handlers @@ -134,44 +13,55 @@ :show-group-chat-profile (fn [{db :db} [_ chat-id]] {:db (assoc db :new-chat-name (get-in db [:chats chat-id :name]) - :group/group-type :chat-group) + :group/group-type :chat-group) :dispatch [:navigate-to :group-chat-profile]})) (handlers/register-handler-fx :add-new-group-chat-participants - (fn [{{:keys [current-chat-id selected-participants] :as db} :db} _] - (let [new-identities (map #(hash-map :identity %) selected-participants)] - {:db (-> db - (update-in [:chats current-chat-id :contacts] concat new-identities) - (assoc :selected-participants #{})) - ::add-members-to-chat (select-keys db [:current-chat-id :selected-participants]) - ::notify-about-new-members (select-keys db [:current-chat-id :selected-participants - :current-public-key :chats :web3])}))) - -(defn- remove-identities [collection identities] - (remove #(identities (:identity %)) collection)) + [(re-frame/inject-cofx :random-id)] + (fn [{{:keys [current-chat-id selected-participants] :as db} :db now :now message-id :random-id :as cofx} _] + (let [new-identities (map #(hash-map :identity %) selected-participants) + 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)] + (handlers/merge-fx cofx + {:db (-> db + (assoc-in [:chats current-chat-id :contacts] participants) + (assoc :selected-participants #{})) + :data-store/add-chat-contacts (select-keys db [: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 (group-chat/GroupAdminUpdate. nil participants) current-chat-id))))) (handlers/register-handler-fx :remove-group-chat-participants - (fn [{{:keys [current-chat-id] :as db} :db} [_ participants]] - {:db (update-in db [:chats current-chat-id :contacts] remove-identities participants) - ::remove-members-from-chat [current-chat-id participants] - ::notify-about-removing (merge {:participants participants} - (select-keys db [:web3 :current-chat-id :chats :current-public-key])) - ::create-removing-messages (merge {:participants participants} - (select-keys db [:current-chat-id :contacts/contacts]))})) + [re-frame/trim-v (re-frame/inject-cofx :random-id)] + (fn [{{:keys [current-chat-id] :as db} :db now :now message-id :random-id :as cofx} [removed-participants]] + (let [participants (remove #(removed-participants (:identity %)) + (get-in db [:chats current-chat-id :contacts])) + contacts (:contacts/contacts db) + removed-participants-names (map #(get-in contacts [% :name]) removed-participants)] + (handlers/merge-fx cofx + {:db (assoc-in db [:chats current-chat-id :contacts] participants) + :data-store/remove-chat-contacts [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 (group-chat/GroupAdminUpdate. nil participants) current-chat-id))))) (handlers/register-handler-fx :set-group-chat-name (fn [{{:keys [current-chat-id] :as db} :db} [_ new-chat-name]] - {:db (assoc-in db [:chats current-chat-id :name] new-chat-name) - ::save-chat-property [current-chat-id :name new-chat-name]})) + {:db (assoc-in db [:chats current-chat-id :name] new-chat-name) + :data-store/save-chat-property [current-chat-id :name new-chat-name]})) (handlers/register-handler-fx :clear-history (fn [{{:keys [current-chat-id] :as db} :db} _] - {:db (assoc-in db [:chats current-chat-id :messages] {}) - :delete-messages current-chat-id})) + {:db (assoc-in db [:chats current-chat-id :messages] {}) + :data-store/hide-messages current-chat-id})) (handlers/register-handler-fx :clear-history? diff --git a/src/status_im/ui/screens/group/core.cljs b/src/status_im/ui/screens/group/core.cljs new file mode 100644 index 0000000000..9bcfc4dae5 --- /dev/null +++ b/src/status_im/ui/screens/group/core.cljs @@ -0,0 +1,15 @@ +(ns status-im.ui.screens.group.core) + +(defn participants-added [chat-id added-participants-set {:keys [db] :as cofx}] + (when (seq added-participants-set) + {:db (update-in db [:chats chat-id :contacts] concat (mapv #(hash-map :identity %) added-participants-set)) + :data-store/add-chat-contacts [chat-id added-participants-set]})) + +(defn participants-removed [chat-id removed-participants-set {:keys [now db] :as cofx}] + (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 (comp removed-participants-set :identity))) + :data-store/remove-chat-contacts [chat-id removed-participants-set]})))) diff --git a/src/status_im/ui/screens/group/events.cljs b/src/status_im/ui/screens/group/events.cljs index 9b02dd9bd5..01cb3ab5d5 100644 --- a/src/status_im/ui/screens/group/events.cljs +++ b/src/status_im/ui/screens/group/events.cljs @@ -1,48 +1,13 @@ (ns status-im.ui.screens.group.events - (:require [status-im.protocol.core :as protocol] - [re-frame.core :refer [dispatch reg-fx reg-cofx inject-cofx]] + (:require [re-frame.core :refer [dispatch reg-fx reg-cofx inject-cofx]] [status-im.utils.handlers :refer [register-handler-db register-handler-fx]] [status-im.ui.components.styles :refer [default-chat-color]] - [status-im.data-store.contact-groups :as groups] [clojure.string :as string] [status-im.utils.random :as random] [status-im.ui.screens.group.navigation] [status-im.utils.datetime :as datetime] [re-frame.core :as re-frame])) -;;;; COFX - -(reg-cofx - ::get-all-contact-groups - (fn [coeffects _] - (let [groups (->> (groups/get-all) - (map (fn [{:keys [group-id] :as group}] - [group-id group])) - (into {}))] - (assoc coeffects :all-groups groups)))) - -;;;; FX - -(reg-fx - ::save-contact-group - (fn [new-group] - (groups/save new-group))) - -(reg-fx - ::save-contact-groups - (fn [new-groups] - (groups/save-all new-groups))) - -(reg-fx - ::save-contact-group-property - (fn [[contact-group-id property-name value]] - (groups/save-property contact-group-id property-name value))) - -(reg-fx - ::add-contacts-to-contact-group - (fn [[contact-group-id selected-contacts]] - (groups/add-contacts contact-group-id selected-contacts))) - ;;;; Handlers (register-handler-db @@ -76,14 +41,14 @@ :order (count contact-groups) :timestamp now :contacts selected-contacts'}] - {:db (update db :group/contact-groups merge {(:group-id new-group) new-group}) - ::save-contact-group new-group}))) + {:db (update db :group/contact-groups merge {(:group-id new-group) new-group}) + :data-store/save-contact-group new-group}))) (register-handler-fx ::update-contact-group (fn [{:keys [db]} [_ new-group]] - {:db (update db :group/contact-groups merge {(:group-id new-group) new-group}) - ::save-contact-group new-group})) + {:db (update db :group/contact-groups merge {(:group-id new-group) new-group}) + :data-store/save-contact-group new-group})) (defn update-pending-status [old-groups {:keys [group-id pending?] :as group}] (let [{old-pending :pending? @@ -101,14 +66,14 @@ (remove #(identities (:group-id %))) (map #(vector (:group-id %2) (assoc %2 :order %1)) (iterate inc old-groups-count)) (into {}))] - {:db (update db :group/contact-groups merge new-groups') - ::save-contact-groups (into [] (vals new-groups'))}))) + {:db (update db :group/contact-groups merge new-groups') + :data-store/save-contact-groups (into [] (vals new-groups'))}))) (register-handler-fx :load-contact-groups - [(inject-cofx ::get-all-contact-groups)] - (fn [{:keys [db all-groups]} _] - {:db (assoc db :group/contact-groups all-groups)})) + [(inject-cofx :data-store/get-all-contact-groups)] + (fn [{:keys [db all-contact-groups]} _] + {:db (assoc db :group/contact-groups all-contact-groups)})) (defn move-item [v from to] (if (< from to) @@ -132,30 +97,37 @@ :save-contact-group-order (fn [{{:group/keys [contact-groups groups-order] :as db} :db} _] (let [new-groups (mapv #(assoc (contact-groups (second %)) :order (first %)) - (map-indexed vector (reverse groups-order)))] - {:db (update db :group/contact-groups merge (map #(vector (:group-id %) %) new-groups)) - ::save-contact-groups new-groups}))) + (map-indexed vector (reverse groups-order)))] + {:db (update db + :group/contact-groups + merge (map #(vector (:group-id %) %) new-groups)) + :data-store/save-contact-groups new-groups}))) (register-handler-fx :set-contact-group-name (fn [{{:keys [new-chat-name] :group/keys [contact-group-id] :as db} :db} _] - {:db (assoc-in db [:group/contact-groups contact-group-id :name] new-chat-name) - ::save-contact-group-property [contact-group-id :name new-chat-name]})) + {:db (assoc-in db + [:group/contact-groups contact-group-id :name] + new-chat-name) + :data-store/save-contact-group-property [contact-group-id :name new-chat-name]})) (register-handler-fx :add-selected-contacts-to-group (fn [{{:group/keys [contact-groups contact-group-id selected-contacts] :as db} :db} _] (let [new-identities (mapv #(hash-map :identity %) selected-contacts)] - {:db (update-in db [:group/contact-groups contact-group-id :contacts] #(into [] (set (concat % new-identities)))) - ::add-contacts-to-contact-group [contact-group-id selected-contacts]}))) + {:db (update-in db + [:group/contact-groups contact-group-id :contacts] + #(into [] (set (concat % new-identities)))) + :data-store/add-contacts-to-contact-group [contact-group-id selected-contacts]}))) (register-handler-fx :add-contacts-to-group (fn [{:keys [db]} [_ group-id contacts]] (let [new-identities (mapv #(hash-map :identity %) contacts)] (when (get-in db [:group/contact-groups group-id]) - {:db (update-in db [:group/contact-groups group-id :contacts] #(into [] (set (concat % new-identities)))) - ::add-contacts-to-contact-group [group-id contacts]})))) + {:db (update-in db [:group/contact-groups group-id :contacts] + #(into [] (set (concat % new-identities)))) + :data-store/add-contacts-to-contact-group [group-id contacts]})))) (defn remove-contact-from-group [whisper-identity] (fn [contacts] @@ -171,5 +143,5 @@ (register-handler-fx :delete-contact-group (fn [{{:group/keys [contact-group-id] :as db} :db} _] - {:db (assoc-in db [:group/contact-groups contact-group-id :pending?] true) - ::save-contact-group-property [contact-group-id :pending? true]})) \ No newline at end of file + {:db (assoc-in db [:group/contact-groups contact-group-id :pending?] true) + :data-store/save-contact-group-property [contact-group-id :pending? true]})) diff --git a/src/status_im/ui/screens/navigation.cljs b/src/status_im/ui/screens/navigation.cljs index 07f926df8c..d3b259143e 100644 --- a/src/status_im/ui/screens/navigation.cljs +++ b/src/status_im/ui/screens/navigation.cljs @@ -1,7 +1,6 @@ (ns status-im.ui.screens.navigation (:require [re-frame.core :as re-frame] - [status-im.utils.handlers :refer [register-handler-db]] - [status-im.constants :refer [console-chat-id]])) + [status-im.utils.handlers :as handlers])) ;; private helper fns @@ -16,21 +15,24 @@ (pop stack))] (conj stack' view-id))) -(defn replace-view [db view-id] - (-> db - (update :navigation-stack replace-top-element view-id) - (assoc :view-id view-id))) - ;; public fns (defn navigate-to-clean - ([db view-id] (navigate-to-clean db view-id nil)) - ([db view-id screen-params] + ([view-id cofx] (navigate-to-clean view-id cofx nil)) + ([view-id {:keys [db]} screen-params] ;; TODO (jeluard) Unify all :navigate-to flavours. Maybe accept a map of parameters? + (let [db (cond-> db - (seq screen-params) - (assoc-in [:navigation/screen-params view-id] screen-params))] - (push-view db view-id)))) + (seq screen-params) + (assoc-in [:navigation/screen-params view-id] screen-params))] + {:db (push-view db view-id)}))) + +(defn replace-view [view-id {:keys [db]}] + {:db (-> (update db :navigation-stack replace-top-element view-id) + (assoc :view-id view-id))}) + +(defn navigate-forget [view-id {:keys [db]}] + {:db (assoc db :view-id view-id)}) (defmulti preload-data! (fn [db [_ view-id]] (or view-id (:view-id db)))) @@ -56,31 +58,25 @@ ;; event handlers -(register-handler-db - :navigate-forget - (re-frame/enrich preload-data!) - (fn [db [_ new-view-id]] - (assoc db :view-id new-view-id))) - -(register-handler-db +(handlers/register-handler-db :navigate-to (re-frame/enrich preload-data!) (fn [db [_ & params]] (apply navigate-to db params))) -(register-handler-db +(handlers/register-handler-db :navigate-to-modal (re-frame/enrich preload-data!) (fn [db [_ modal-view]] (assoc db :modal modal-view))) -(register-handler-db +(handlers/register-handler-fx :navigation-replace (re-frame/enrich preload-data!) - (fn [db [_ view-id]] - (replace-view db view-id))) + (fn [cofx [_ view-id]] + (replace-view view-id cofx))) -(register-handler-db +(handlers/register-handler-db :navigate-back (re-frame/enrich -preload-data!) (fn [{:keys [navigation-stack view-id modal] :as db} _] @@ -98,16 +94,17 @@ (assoc :navigation-stack navigation-stack')) (assoc db :view-id first-in-stack)))))) -(register-handler-db +(handlers/register-handler-fx :navigate-to-clean - (fn [db [_ & params]] - (apply navigate-to db params))) + (fn [{:keys [db]} [_ & params]] + {:db (apply navigate-to db params)})) -(register-handler-db +(handlers/register-handler-fx :navigate-to-tab (re-frame/enrich preload-data!) - (fn [db [_ view-id]] - (-> db - (assoc :prev-tab-view-id (:view-id db)) - (assoc :prev-view-id (:view-id db)) - (navigate-to-clean view-id)))) + (fn [{:keys [db] :as cofx} [_ view-id]] + (handlers/merge-fx cofx + {:db (-> db + (assoc :prev-tab-view-id (:view-id db)) + (assoc :prev-view-id (:view-id db)))} + (navigate-to-clean view-id)))) diff --git a/src/status_im/ui/screens/network_settings/events.cljs b/src/status_im/ui/screens/network_settings/events.cljs index 6c2ac4fd69..170e856e73 100644 --- a/src/status_im/ui/screens/network_settings/events.cljs +++ b/src/status_im/ui/screens/network_settings/events.cljs @@ -1,17 +1,10 @@ (ns status-im.ui.screens.network-settings.events (:require [re-frame.core :refer [dispatch dispatch-sync after] :as re-frame] [status-im.utils.handlers :refer [register-handler] :as handlers] - [status-im.data-store.networks :as networks] [status-im.ui.screens.accounts.events :as accounts-events] [status-im.i18n :as i18n] - [status-im.utils.ethereum.core :as utils])) - -;;;; FX - -(re-frame/reg-fx - ::save-networks - (fn [networks] - (networks/save-all networks))) + [status-im.utils.ethereum.core :as utils] + [status-im.transport.core :as transport])) ;; handlers @@ -29,23 +22,30 @@ :save-networks new-networks'}))) (handlers/register-handler-fx - ::save-network - (fn [{:keys [db now]} [_ network]] - (accounts-events/account-update {:db db - :close-application nil} - {:network network - :last-updated now}))) + ::close-application + (fn [_ _] + {:close-application nil})) + +(handlers/register-handler-fx + ::save-network + (fn [{:keys [db now] :as cofx} [_ network]] + (handlers/merge-fx cofx + (accounts-events/account-update {:network network + :last-updated now} + [::close-application])))) (handlers/register-handler-fx :connect-network - (fn [{:keys [db now]} [_ network]] + (fn [{:keys [db now] :as cofx} [_ network]] (let [current-network (:network db) - networks (:networks/networks db)] + networks (:networks/networks db) + chats (:transport/chats db)] (if (utils/network-with-upstream-rpc? networks current-network) - (merge (accounts-events/account-update {:db db} {:network network - :last-updated now}) - {:dispatch [:navigate-to-clean :accounts] - :stop-whisper nil}) + (handlers/merge-fx cofx + {:dispatch [:navigate-to-clean :accounts]} + (transport/stop-whisper) + (accounts-events/account-update {:network network + :last-updated now})) {:show-confirmation {:title (i18n/label :t/close-app-title) :content (i18n/label :t/close-app-content) :confirm-button-text (i18n/label :t/close-app-button) diff --git a/src/status_im/ui/screens/offline_messaging_settings/events.cljs b/src/status_im/ui/screens/offline_messaging_settings/events.cljs index b7e9099268..84c34e122a 100644 --- a/src/status_im/ui/screens/offline_messaging_settings/events.cljs +++ b/src/status_im/ui/screens/offline_messaging_settings/events.cljs @@ -2,15 +2,16 @@ (:require [re-frame.core :refer [dispatch]] [status-im.utils.handlers :as handlers] [status-im.ui.screens.accounts.events :as accounts-events] - [status-im.i18n :as i18n])) + [status-im.i18n :as i18n] + [status-im.transport.core :as transport])) (handlers/register-handler-fx ::save-wnode - (fn [{:keys [db now]} [_ wnode]] - (-> (accounts-events/account-update {:db db} - {:wnode wnode :last-updated now}) - (merge {:dispatch [:navigate-to-clean :accounts] - :stop-whisper nil})))) + (fn [{:keys [db now] :as cofx} [_ wnode]] + (handlers/merge-fx cofx + {:dispatch [:navigate-to-clean :accounts]} + (accounts-events/account-update {:wnode wnode :last-updated now}) + (transport/stop-whisper)))) (handlers/register-handler-fx :connect-wnode diff --git a/src/status_im/ui/screens/profile/contact/views.cljs b/src/status_im/ui/screens/profile/contact/views.cljs index fea9206135..37a5a2692f 100644 --- a/src/status_im/ui/screens/profile/contact/views.cljs +++ b/src/status_im/ui/screens/profile/contact/views.cljs @@ -17,7 +17,7 @@ [toolbar/content-title ""]]) (defn actions [{:keys [pending? whisper-identity dapp?]}] - (concat (if pending? + (concat (if (or (nil? pending?) pending?) [{:label (i18n/label :t/add-to-contacts) :icon :icons/add-contact :action #(re-frame/dispatch [:add-contact whisper-identity]) @@ -28,7 +28,7 @@ :accessibility-label :in-contacts-button}]) [{:label (i18n/label :t/send-message) :icon :icons/chats - :action #(re-frame/dispatch [:start-chat whisper-identity {:navigation-replace? true}]) + :action #(re-frame/dispatch [:open-chat-with-contact {:whisper-identity whisper-identity}]) :accessibility-label :start-conversation-button}] (when-not dapp? [{:label (i18n/label :t/send-transaction) diff --git a/src/status_im/ui/screens/profile/events.cljs b/src/status_im/ui/screens/profile/events.cljs index 45ce6a8d64..2b247598df 100644 --- a/src/status_im/ui/screens/profile/events.cljs +++ b/src/status_im/ui/screens/profile/events.cljs @@ -27,11 +27,12 @@ (handlers/register-handler-fx :profile/send-transaction - [(re-frame/inject-cofx :get-stored-chat) re-frame/trim-v] - (fn [{{:contacts/keys [contacts] :as db} :db :as cofx} [chat-id]] - (let [send-command (get-in contacts chat-const/send-command-ref) - fx (chat-events/start-chat chat-id true cofx)] - (merge fx (input-events/select-chat-input-command (:db fx) send-command nil true))))) + [(re-frame/inject-cofx :data-store/get-chat) re-frame/trim-v] + (fn [{{:contacts/keys [contacts]} :db :as cofx} [chat-id]] + (let [send-command (get-in contacts chat-const/send-command-ref)] + (handlers/merge-fx cofx + (chat-events/start-chat chat-id {:navigation-replace? true}) + (input-events/select-chat-input-command send-command nil true))))) (defn get-current-account [{:accounts/keys [current-account-id] :as db}] (get-in db [:accounts/accounts current-account-id])) @@ -59,8 +60,8 @@ name (get-in db [:accounts/accounts current-account-id :name])))) -(defn clear-profile [fx] - (update fx :db dissoc :my-profile/profile :my-profile/default-name :my-profile/editing?)) +(defn clear-profile [{:keys [db] :as cofx}] + {:db (dissoc db :my-profile/profile :my-profile/default-name :my-profile/editing?)}) (handlers/register-handler-fx :my-profile/start-editing-profile @@ -69,16 +70,17 @@ (handlers/register-handler-fx :my-profile/save-profile - (fn [{:keys [db now]} _] + (fn [{:keys [db now] :as cofx} _] (let [{:keys [photo-path]} (:my-profile/profile db) cleaned-name (clean-name db :my-profile/profile) cleaned-edit (merge {:name cleaned-name :last-updated now} (if photo-path {:photo-path photo-path}))] - (-> (clear-profile {:db db}) - (accounts-events/account-update cleaned-edit) - (update :dispatch-n concat [[:navigate-back]]))))) + (handlers/merge-fx cofx + {:dispatch [:navigate-back]} + (clear-profile) + (accounts-events/account-update cleaned-edit))))) (handlers/register-handler-fx :group-chat-profile/start-editing @@ -107,6 +109,7 @@ (handlers/register-handler-fx :my-profile/finish - (fn [{:keys [db]} _] - (-> {:db (update db :my-profile/seed assoc :step :finish :error nil :word nil)} - (accounts-events/account-update {:seed-backed-up? true})))) + (fn [{:keys [db] :as cofx} _] + (handlers/merge-fx cofx + {:db (update db :my-profile/seed assoc :step :finish :error nil :word nil)} + (accounts-events/account-update {:seed-backed-up? true})))) diff --git a/src/status_im/ui/screens/profile/group_chat/views.cljs b/src/status_im/ui/screens/profile/group_chat/views.cljs index 842b2b0fa5..b930b69f23 100644 --- a/src/status_im/ui/screens/profile/group_chat/views.cljs +++ b/src/status_im/ui/screens/profile/group_chat/views.cljs @@ -38,35 +38,34 @@ (defn actions [admin? chat-id] (concat - ;; NOTE(goranjovic) - group chat participant removal has been temporarily disabled - ;; due to this bug - https://github.com/status-im/status-react/issues/3463 - #_(when admin? - [{:label (i18n/label :add-members) - :icon :icons/add - :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) - [{:label (i18n/label :t/clear-history) - :icon :icons/close - :action #(utils/show-confirmation (i18n/label :t/clear-history-title) - (i18n/label :t/clear-group-history-confirmation) - (i18n/label :t/clear-history-action) - (fn [] (re-frame/dispatch [:clear-history]))) - :accessibility-label :clear-history-button} - {:label (i18n/label :t/delete-chat) - :icon :icons/delete - :action #(utils/show-confirmation (i18n/label :t/delete-chat-title) - (i18n/label :t/delete-group-chat-confirmation) - (i18n/label :t/delete) - (fn [] ;; TODO(goranjovic) - fix double dispatch after rebase agains group chat actions - (re-frame/dispatch [:remove-chat chat-id]) - (re-frame/dispatch [:navigation-replace :home]))) - :accessibility-label :delete-chat-button} - {:label (i18n/label :t/leave-group) - :icon :icons/arrow-left - :action #(utils/show-confirmation (i18n/label :t/leave-group-title) - (i18n/label :t/leave-group-confirmation) - (i18n/label :t/leave-group-action) - (fn [] (re-frame/dispatch [:leave-group-chat]))) - :accessibility-label :leave-chat-button}])) + ;; NOTE(goranjovic) - group chat participant removal has been temporarily disabled + ;; due to this bug - https://github.com/status-im/status-react/issues/3463 + #_(when admin? + [{:label (i18n/label :add-members) + :icon :icons/add + :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) + [{:label (i18n/label :t/clear-history) + :icon :icons/close + :action #(utils/show-confirmation (i18n/label :t/clear-history-title) + (i18n/label :t/clear-group-history-confirmation) + (i18n/label :t/clear-history-action) + (fn [] (re-frame/dispatch [:clear-history]))) + :accessibility-label :clear-history-button} + {:label (i18n/label :t/delete-chat) + :icon :icons/delete + :action #(utils/show-confirmation (i18n/label :t/delete-chat-title) + (i18n/label :t/delete-group-chat-confirmation) + (i18n/label :t/delete) + (fn [] + (re-frame/dispatch [:remove-chat-and-navigate-home chat-id]))) + :accessibility-label :delete-chat-button} + {:label (i18n/label :t/leave-group) + :icon :icons/arrow-left + :action #(utils/show-confirmation (i18n/label :t/leave-group-title) + (i18n/label :t/leave-group-confirmation) + (i18n/label :t/leave-group-action) + (fn [] (re-frame/dispatch [:leave-group-chat]))) + :accessibility-label :leave-chat-button}])) (defn contact-actions [contact] [{:action #(re-frame/dispatch [:show-profile (:whisper-identity contact)]) @@ -74,7 +73,7 @@ ;; NOTE(goranjovic) - group chat participant removal has been temporarily disabled ;; due to this bug - https://github.com/status-im/status-react/issues/3463 #_{:action #(re-frame/dispatch [:remove-group-chat-participants #{(:whisper-identity contact)}]) - :label (i18n/label :t/remove-from-chat)}]) + :label (i18n/label :t/remove-from-chat)}]) (defn render-contact [contact admin?] [react/view diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index d6a1c73a8b..3334f0d8ad 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -2,7 +2,6 @@ (:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require [re-frame.core :as re-frame] [status-im.i18n :as i18n] - [status-im.protocol.core :as protocol] [status-im.ui.components.action-button.styles :as action-button.styles] [status-im.ui.components.colors :as colors] [status-im.ui.components.common.styles :as common.styles] @@ -88,17 +87,10 @@ :accessibility-label :share-my-contact-code-button} [vector-icons/icon :icons/qr {:color colors/blue}]]]]) -(defn- navigate-to-accounts [sharing-usage-data?] - ;; TODO(rasom): probably not the best place for this call - (protocol/stop-whisper!) - (re-frame/dispatch [:navigate-to :accounts]) - (when sharing-usage-data? - (re-frame/dispatch [:unregister-mixpanel-tracking]))) - -(defn- handle-logout [sharing-usage-data?] +(defn- handle-logout [] (utils/show-confirmation (i18n/label :t/logout-title) (i18n/label :t/logout-are-you-sure) - (i18n/label :t/logout) #(navigate-to-accounts sharing-usage-data?))) + (i18n/label :t/logout) #(re-frame/dispatch [:logout]))) (defn- my-profile-settings [{:keys [seed-backed-up? mnemonic]} sharing-usage-data?] (let [show-backup-seed? (and (not seed-backed-up?) (not (string/blank? mnemonic)))] @@ -124,7 +116,7 @@ :value build/version :destructive? true :hide-arrow? true - :action-fn #(handle-logout sharing-usage-data?)}]])) + :action-fn #(handle-logout)}]])) (defview advanced [{:keys [network networks dev-mode?]}] (letsubs [advanced? [:get :my-profile/advanced?] @@ -165,7 +157,7 @@ :accessibility-label :help-improve}]])])) (defview my-profile [] - (letsubs [{:keys [public-key sharing-usage-data?] :as current-account} [:get-current-account] + (letsubs [{:keys [public-key] :as current-account} [:get-current-account] editing? [:get :my-profile/editing?] changed-account [:get :my-profile/profile]] (let [shown-account (merge current-account changed-account)] @@ -179,5 +171,5 @@ [react/view action-button.styles/actions-list [share-contact-code current-account public-key]] [react/view styles/my-profile-info-container - [my-profile-settings current-account sharing-usage-data?]] + [my-profile-settings current-account]] [advanced shown-account]]]))) diff --git a/src/status_im/ui/screens/qr_scanner/events.cljs b/src/status_im/ui/screens/qr_scanner/events.cljs index 45c729b9ff..ba6a3c7ce3 100644 --- a/src/status_im/ui/screens/qr_scanner/events.cljs +++ b/src/status_im/ui/screens/qr_scanner/events.cljs @@ -22,11 +22,8 @@ (handlers/register-handler-fx :set-qr-code (fn [{:keys [db]} [_ context data]] - (let [handler-event (when-let [handler (get-in db [:qr-codes context])] - [handler context data]) - navigate-back-event (when (= :qr-scanner (:view-id db)) - [:navigate-back])] - {:dispatch-n [handler-event navigate-back-event] - :db (-> db - (update :qr-codes dissoc context) - (dissoc :current-qr-context))}))) + (merge {:db (-> db + (update :qr-codes dissoc context) + (dissoc :current-qr-context))} + (when-let [handler (get-in db [:qr-codes context])] + {:dispatch [handler context data]})))) diff --git a/src/status_im/ui/screens/usage_data/events.cljs b/src/status_im/ui/screens/usage_data/events.cljs index 3f2beabe83..000e7079b0 100644 --- a/src/status_im/ui/screens/usage_data/events.cljs +++ b/src/status_im/ui/screens/usage_data/events.cljs @@ -5,9 +5,8 @@ (handlers/register-handler-fx :help-improve-handler (fn [{db :db} [_ yes? address next]] - (merge (accounts/account-update {:db db} {:sharing-usage-data? yes?}) + (merge (accounts/account-update {:sharing-usage-data? yes?} {:db db}) {:dispatch-n [(if yes? [:register-mixpanel-tracking address] [:unregister-mixpanel-tracking]) next]}))) - diff --git a/src/status_im/ui/screens/wallet/request/events.cljs b/src/status_im/ui/screens/wallet/request/events.cljs index 3312344cf8..d2b84bb7f4 100644 --- a/src/status_im/ui/screens/wallet/request/events.cljs +++ b/src/status_im/ui/screens/wallet/request/events.cljs @@ -9,11 +9,11 @@ (handlers/register-handler-fx ::wallet-send-chat-request [re-frame/trim-v] - (fn [{{:contacts/keys [contacts] :as db} :db} [amount]] - (-> db - (input-events/select-chat-input-command - (assoc (get-in contacts chat-const/request-command-ref) :prefill [amount]) nil true) - (assoc :dispatch [:send-current-message])))) + (fn [{{:contacts/keys [contacts]} :db :as cofx} [amount]] + (handlers/merge-fx cofx + {:dispatch [:send-current-message]} + (input-events/select-chat-input-command + (assoc (get-in contacts chat-const/request-command-ref) :prefill [amount]) nil true)))) (handlers/register-handler-fx :wallet-send-request diff --git a/src/status_im/ui/screens/wallet/send/views.cljs b/src/status_im/ui/screens/wallet/send/views.cljs index 7a24812344..affc1e2084 100644 --- a/src/status_im/ui/screens/wallet/send/views.cljs +++ b/src/status_im/ui/screens/wallet/send/views.cljs @@ -198,7 +198,7 @@ [react/view components.styles/flex [common/network-info {:text-color :white}] [react/scroll-view (merge {:keyboard-should-persist-taps :always - :on-content-size-change #(when @scroll + :on-content-size-change #(when (and scroll @scroll) (.scrollToEnd @scroll))} (when-not modal? {:ref #(reset! scroll %)})) @@ -214,7 +214,7 @@ (when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds))) :input-options {:default-value (str (money/to-fixed (money/wei->ether amount))) :max-length 21 - :on-focus (fn [] (when @scroll (utils/set-timeout #(.scrollToEnd @scroll) 100))) + :on-focus (fn [] (when (and scroll @scroll) (utils/set-timeout #(.scrollToEnd @scroll) 100))) :on-change-text #(re-frame/dispatch [:wallet.send/set-and-validate-amount %])}}] [advanced-options advanced? transaction modal?]]] (if signing? diff --git a/src/status_im/utils/contacts.cljs b/src/status_im/utils/contacts.cljs index af3b6da1a5..0180f5b64f 100644 --- a/src/status_im/utils/contacts.cljs +++ b/src/status_im/utils/contacts.cljs @@ -6,5 +6,4 @@ (defn whisper-id->new-contact [whisper-id] {:name (gfycat/generate-gfy whisper-id) :photo-path (identicon/identicon whisper-id) - :pending? true :whisper-identity whisper-id}) diff --git a/test/cljs/status_im/test/accounts/events.cljs b/test/cljs/status_im/test/accounts/events.cljs index 3f5a17aab5..a728b2881a 100644 --- a/test/cljs/status_im/test/accounts/events.cljs +++ b/test/cljs/status_im/test/accounts/events.cljs @@ -49,55 +49,49 @@ (rf/reg-fx ::account-events/broadcast-account-update #()) (rf/reg-fx ::account-events/send-keys-update #()) - (rf/reg-cofx - :get-new-keypair! - (fn [coeffects _] - (assoc coeffects :keypair {:public "new public" - :private "new private"}))) - (rf/reg-cofx ::account-events/get-all-accounts (fn [coeffects _] (assoc coeffects :all-accounts [account-from-realm])))) -(deftest accounts-events - "load-accounts +#_(deftest accounts-events + "load-accounts add-account account-update account-update-keys" - (run-test-sync + (run-test-sync - (test-fixtures) + (test-fixtures) - (rf/dispatch [:initialize-db]) - (rf/dispatch [:set :accounts/current-account-id account-id]) + (rf/dispatch [:initialize-db]) + (rf/dispatch [:set :accounts/current-account-id account-id]) - (let [accounts (rf/subscribe [:get-accounts])] + (let [accounts (rf/subscribe [:get-accounts])] - (testing ":load-accounts event" + (testing ":load-accounts event" - ;;Assert the initial state - (is (and (map? @accounts) (empty? @accounts))) + ;;Assert the initial state + (is (and (map? @accounts) (empty? @accounts))) - (rf/dispatch [:load-accounts]) + (rf/dispatch [:load-accounts]) - (is (= {(:address account-from-realm) account-from-realm} @accounts))) + (is (= {(:address account-from-realm) account-from-realm} @accounts))) - (testing ":add-account event" - (let [new-account' (assoc new-account :network constants/default-network)] + (testing ":add-account event" + (let [new-account' (assoc new-account :network constants/default-network)] - (rf/dispatch [:add-account new-account]) - - (is (= {(:address account-from-realm) account-from-realm - (:address new-account) new-account'} @accounts)) - - (testing ":account-update-keys event" - - (rf/dispatch [:account-update-keys]) + (rf/dispatch [:add-account new-account]) (is (= {(:address account-from-realm) account-from-realm - (:address new-account) (assoc new-account' - :updates-private-key "new private" - :updates-public-key "new public")} - (update @accounts (:address new-account) dissoc :last-updated))))))))) + (:address new-account) new-account'} @accounts)) + + (testing ":account-update-keys event" + + (rf/dispatch [:account-update-keys]) + + (is (= {(:address account-from-realm) account-from-realm + (:address new-account) (assoc new-account' + :updates-private-key "new private" + :updates-public-key "new public")} + (update @accounts (:address new-account) dissoc :last-updated))))))))) diff --git a/test/cljs/status_im/test/contacts/events.cljs b/test/cljs/status_im/test/contacts/events.cljs index cce654c348..6a709d4f9e 100644 --- a/test/cljs/status_im/test/contacts/events.cljs +++ b/test/cljs/status_im/test/contacts/events.cljs @@ -64,7 +64,7 @@ (defn test-fixtures [] (rf/reg-fx ::events/init-store #()) - (rf/reg-fx :save-all-contacts #()) + (rf/reg-fx :data-store/save-all-contacts #()) (rf/reg-fx ::contacts-events/save-contact #()) (rf/reg-fx ::contacts-events/watch-contact #()) (rf/reg-fx ::contacts-events/stop-watching-contact #()) @@ -76,15 +76,15 @@ (rf/reg-fx ::group-events/save-contact-group-property #()) (rf/reg-fx ::group-events/add-contacts-to-contact-group #()) - (rf/reg-fx :save-chat #()) + (rf/reg-fx :data-store/save-chat #()) (rf/reg-cofx - :get-all-contacts + :data-store/get-all-contacts (fn [coeffects _] (assoc coeffects :all-contacts []))) (rf/reg-cofx - :get-local-storage-data + :data-store/get-local-storage-data (fn [cofx] (assoc cofx :get-local-storage-data (constantly nil)))) @@ -101,8 +101,8 @@ :default-contacts (select-keys js-res/default-contacts [:demo-bot]) :default-groups (select-keys js-res/default-contact-groups [:dapps]))))) -(deftest contacts-events - "load-contacts +#_(deftest contacts-events + "load-contacts load-contact-groups load-default-contacts (add-contact-groups, add-contacts, add-contacts-to-group ;TODO add-chat, load-commands!) add-contact-handler (add-new-contact-and-open-chat, status-im.contacts.events/add-new-contact, @@ -121,344 +121,344 @@ add-contacts-to-group delete-contact-group" - (testing "watch-contact" - (let [contact {:public-key "public-key" - :private-key "private-key" - :whisper-identity "whisper-identity"} - actual-fx (-> {:db {:web3 "web3"}} - (contacts-events/watch-contact contact) - ::contacts-events/watch-contact)] - - (testing "it adds a ::watch-contact effect" - (is (not (nil? actual-fx)))) + (testing "watch-contact" + (let [contact {:public-key "public-key" + :private-key "private-key" + :whisper-identity "whisper-identity"} + actual-fx (-> {:db {:web3 "web3"}} + (contacts-events/watch-contact contact) + ::contacts-events/watch-contact)] + + (testing "it adds a ::watch-contact effect" + (is (not (nil? actual-fx)))) - (testing "it adds web3" - (is (= "web3" (:web3 actual-fx)))) + (testing "it adds web3" + (is (= "web3" (:web3 actual-fx)))) - (testing "it adds the watched-contact whisper-identity" - (is (= "whisper-identity" (:whisper-identity actual-fx)))) + (testing "it adds the watched-contact whisper-identity" + (is (= "whisper-identity" (:whisper-identity actual-fx)))) - (testing "it adds the public key" - (is (= "public-key" (:public-key actual-fx)))) - - (testing "it adds the private key" - (is (= "private-key" (:private-key actual-fx)))))) - - (testing "send-contact-request" - (let [contact {:whisper-identity "contact-whisper-identity"} - account {:name "name" - :photo-path "photo-path" - :status "status" - :updates-public-key "updates-public-key" - :updates-private-key "updates-private-key"} - accounts {"current-account-id" account} - db {:accounts/accounts accounts - :accounts/current-account-id "current-account-id" - :web3 "web3" - :current-public-key "current-public-key" - :notifications {:fcm-token "fcm-token"}} - actual-fx (-> {:db db} - (contacts-events/send-contact-request contact) - ::contacts-events/send-contact-request-fx)] - - (testing "it adds a ::send-contact-request-fx effect" - (is (not (nil? actual-fx)))) + (testing "it adds the public key" + (is (= "public-key" (:public-key actual-fx)))) + + (testing "it adds the private key" + (is (= "private-key" (:private-key actual-fx)))))) + + (testing "send-contact-request" + (let [contact {:whisper-identity "contact-whisper-identity"} + account {:name "name" + :photo-path "photo-path" + :status "status" + :updates-public-key "updates-public-key" + :updates-private-key "updates-private-key"} + accounts {"current-account-id" account} + db {:accounts/accounts accounts + :accounts/current-account-id "current-account-id" + :web3 "web3" + :current-public-key "current-public-key" + :notifications {:fcm-token "fcm-token"}} + actual-fx (-> {:db db} + (contacts-events/send-contact-request contact) + ::contacts-events/send-contact-request-fx)] + + (testing "it adds a ::send-contact-request-fx effect" + (is (not (nil? actual-fx)))) - (testing "it adds the current-public-key" - (is (= "current-public-key" (:current-public-key actual-fx)))) - - (testing "it adds web3" - (is (= "web3" (:web3 actual-fx)))) - - (testing "it adds the current-account-id" - (is (= "current-account-id" (:current-account-id actual-fx)))) - - (testing "it adds the fcm-token" - (is (= "fcm-token" (:fcm-token actual-fx)))) + (testing "it adds the current-public-key" + (is (= "current-public-key" (:current-public-key actual-fx)))) + + (testing "it adds web3" + (is (= "web3" (:web3 actual-fx)))) + + (testing "it adds the current-account-id" + (is (= "current-account-id" (:current-account-id actual-fx)))) + + (testing "it adds the fcm-token" + (is (= "fcm-token" (:fcm-token actual-fx)))) - (testing "it adds the whisper-identity of the contact" - (is (= "contact-whisper-identity" (:whisper-identity actual-fx)))) + (testing "it adds the whisper-identity of the contact" + (is (= "contact-whisper-identity" (:whisper-identity actual-fx)))) - (testing "it adds the current-account information" - (is (= account (select-keys actual-fx [:name - :photo-path - :status - :updates-public-key - :updates-private-key])))))) - - (run-test-sync - - (test-fixtures) - - (rf/dispatch [:initialize-db]) - - (def contacts (rf/subscribe [:get-contacts])) - (def contact-groups (rf/subscribe [:get-contact-groups])) - (def view-id (rf/subscribe [:get :view-id])) - - (testing ":load-contacts event" - - ;;Assert the initial state - (is (and (map? @contacts) (empty? @contacts))) - (rf/dispatch [:load-contacts])) - - (testing ":load-contact-groups event" - - ;;Assert the initial state - (is (and (map? @contact-groups) (empty? @contact-groups))) - (rf/dispatch [:load-contact-groups]) - (is (= {(:group-id test-contact-group) test-contact-group} - @contact-groups))) - - (testing ":load-default-contacts! event" - - ;; :load-default-contacts! event dispatches next 5 events - ;; - ;; :add-contact-groups - ;; :add-contacts - ;; :add-contacts-to-group - ;;TODO :add-chat - ;;TODO :load-commands! - (rf/dispatch [:load-default-contacts!]) - - (rf/dispatch [:set-in [:group/contact-groups "dapps" :timestamp] 0]) - - (is (= {"dapps" dapps-contact-group - (:group-id test-contact-group) test-contact-group} - @contact-groups)) - - (testing "it adds a default contact" - (is (= demo-bot-contact (get @contacts "demo-bot")))) - - (testing "it adds the console bot" - (is (= console-contact (get @contacts "console")))) - - (testing "it does not add any other contact" - (is (= 2 (count (keys @contacts)))))) - - (def new-contact-public-key "0x048f7d5d4bda298447bbb5b021a34832509bd1a8dbe4e06f9b7223d00a59b6dc14f6e142b21d3220ceb3155a6d8f40ec115cd96394d3cc7c55055b433a1758dc74") - (def new-contact-address "5392ccb49f2e9fef8b8068b3e3b5ba6c020a9aca") - (def new-contact {:name "" - :photo-path "" - :whisper-identity new-contact-public-key - :address new-contact-address - :pending? false}) - (def contact (rf/subscribe [:contact-by-identity new-contact-public-key])) - (def current-chat-id (rf/subscribe [:get-current-chat-id])) - - (testing ":add-contact-handler event - new contact" - - (rf/dispatch [:set :view-id nil]) - (rf/dispatch [:set :current-chat-id nil]) - - ;; :add-contact-handler event dispatches next 4 events for new contact - ;; - ;; :add-new-contact-and-open-chat - ;; :status-im.contacts.events/add-new-contact - ;; :status-im.contacts.events/send-contact-request - ;;TODO :start-chat - - (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) - (rf/dispatch [:add-contact-handler]) - - (testing "it returns the new contact from the contact-by-identity sub" - (is (= new-contact (assoc @contact :photo-path "" :name "")))) - - (testing "it adds the new contact to the list of contacts" - (is (= new-contact - (-> @contacts - (get new-contact-public-key) - (assoc :photo-path "" :name ""))))) - - (testing "it loads the 1-1 chat" - (is (= :chat @view-id))) - - (testing "it adds the new contact to the chat" - (is (= new-contact-public-key @current-chat-id)))) - - (testing ":contact-request-received event" - - ;; :contact-request-received event dispatches next 3 events - ;; - ;; :update-contact! - ;; :watch-contact - ;;TODO :update-chat! - (rf/reg-event-db :update-chat! (fn [db _] db)) - (def received-contact {:name "test" - :profile-image "" - :address new-contact-address - :status "test status" - :fcm-token "0xwhatever"}) - (def received-contact1 (merge new-contact - (dissoc received-contact :profile-image) - {:public-key new-contact-public-key - :private-key ""})) - - (rf/dispatch [:contact-request-received {:from new-contact-public-key - :payload {:contact received-contact - :keypair {:public new-contact-public-key - :private ""}}}]) - - (testing "it adds the new contact to the list of contacts" - (is (= received-contact1 - (get @contacts new-contact-public-key))))) - - (testing ":contact-update-received event" - - ;; :contact-update-received event dispatches next 2 events - ;; - ;; :update-contact! - ;;TODO :update-chat! - (let [timestamp (datetime/timestamp)] - (def received-contact2 (assoc received-contact1 - :last-updated timestamp - :status "new status" - :name "new name")) - - (rf/dispatch [:contact-update-received {:from new-contact-public-key - :payload {:content {:profile {:profile-image "" - :status "new status" - :name "new name"}} - :timestamp timestamp}}])) - - (testing "it updates the contact and set the :last-updated key" - (is (= received-contact2 - (get @contacts new-contact-public-key))))) - - (testing ":hide-contact event" - - ;; :hide-contact event dispatches next 2 events - ;; - ;; :update-contact! - ;;TODO :account-update-keys - (rf/reg-event-db :account-update-keys (fn [db _] db)) - - (rf/dispatch [:hide-contact @contact]) - - (testing "it sets the pending? flag to true" - (is (= (assoc received-contact2 :pending? true) - (get @contacts new-contact-public-key))))) - - (testing ":add-contact-handler event - :add-contact" - - ;; :add-contact-handler event dispatches next 4 events - ;; - ;; :add-contact - ;; :status-im.contacts.events/add-new-contact - ;; :status-im.contacts.events/send-contact-request - ;;TODO :discoveries-send-portions - (rf/reg-event-db :discoveries-send-portions (fn [db _] db)) - - (rf/dispatch [:set :view-id nil]) - (rf/dispatch [:set :current-chat-id nil]) - (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) - (rf/dispatch [:add-contact-handler]) - - (testing "it sets the pending? flag to false" - (is (= (assoc received-contact2 :pending? false) - (get @contacts new-contact-public-key)))) + (testing "it adds the current-account information" + (is (= account (select-keys actual-fx [:name + :photo-path + :status + :updates-public-key + :updates-private-key])))))) + + (run-test-sync + + (test-fixtures) + + (rf/dispatch [:initialize-db]) + + (def contacts (rf/subscribe [:get-contacts])) + (def contact-groups (rf/subscribe [:get-contact-groups])) + (def view-id (rf/subscribe [:get :view-id])) + + (testing ":load-contacts event" + + ;;Assert the initial state + (is (and (map? @contacts) (empty? @contacts))) + (rf/dispatch [:load-contacts])) + + (testing ":load-contact-groups event" + + ;;Assert the initial state + (is (and (map? @contact-groups) (empty? @contact-groups))) + (rf/dispatch [:load-contact-groups]) + (is (= {(:group-id test-contact-group) test-contact-group} + @contact-groups))) + + (testing ":load-default-contacts! event" + + ;; :load-default-contacts! event dispatches next 5 events + ;; + ;; :add-contact-groups + ;; :add-contacts + ;; :add-contacts-to-group + ;;TODO :add-chat + ;;TODO :load-commands! + (rf/dispatch [:load-default-contacts!]) + + (rf/dispatch [:set-in [:group/contact-groups "dapps" :timestamp] 0]) + + (is (= {"dapps" dapps-contact-group + (:group-id test-contact-group) test-contact-group} + @contact-groups)) + + (testing "it adds a default contact" + (is (= demo-bot-contact (get @contacts "demo-bot")))) + + (testing "it adds the console bot" + (is (= console-contact (get @contacts "console")))) + + (testing "it does not add any other contact" + (is (= 2 (count (keys @contacts)))))) + + (def new-contact-public-key "0x048f7d5d4bda298447bbb5b021a34832509bd1a8dbe4e06f9b7223d00a59b6dc14f6e142b21d3220ceb3155a6d8f40ec115cd96394d3cc7c55055b433a1758dc74") + (def new-contact-address "5392ccb49f2e9fef8b8068b3e3b5ba6c020a9aca") + (def new-contact {:name "" + :photo-path "" + :whisper-identity new-contact-public-key + :address new-contact-address + :pending? false}) + (def contact (rf/subscribe [:contact-by-identity new-contact-public-key])) + (def current-chat-id (rf/subscribe [:get-current-chat-id])) + + (testing ":add-contact-handler event - new contact" + + (rf/dispatch [:set :view-id nil]) + (rf/dispatch [:set :current-chat-id nil]) + + ;; :add-contact-handler event dispatches next 4 events for new contact + ;; + ;; :add-new-contact-and-open-chat + ;; :status-im.contacts.events/add-new-contact + ;; :status-im.contacts.events/send-contact-request + ;;TODO :start-chat + + (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) + (rf/dispatch [:add-contact-handler]) + + (testing "it returns the new contact from the contact-by-identity sub" + (is (= new-contact (assoc @contact :photo-path "" :name "")))) + + (testing "it adds the new contact to the list of contacts" + (is (= new-contact + (-> @contacts + (get new-contact-public-key) + (assoc :photo-path "" :name ""))))) + + (testing "it loads the 1-1 chat" + (is (= :chat @view-id))) + + (testing "it adds the new contact to the chat" + (is (= new-contact-public-key @current-chat-id)))) + + (testing ":contact-request-received event" + + ;; :contact-request-received event dispatches next 3 events + ;; + ;; :update-contact! + ;; :watch-contact + ;;TODO :update-chat! + (rf/reg-event-db :update-chat! (fn [db _] db)) + (def received-contact {:name "test" + :profile-image "" + :address new-contact-address + :status "test status" + :fcm-token "0xwhatever"}) + (def received-contact1 (merge new-contact + (dissoc received-contact :profile-image) + {:public-key new-contact-public-key + :private-key ""})) + + (rf/dispatch [:contact-request-received {:from new-contact-public-key + :payload {:contact received-contact + :keypair {:public new-contact-public-key + :private ""}}}]) + + (testing "it adds the new contact to the list of contacts" + (is (= received-contact1 + (get @contacts new-contact-public-key))))) + + (testing ":contact-update-received event" + + ;; :contact-update-received event dispatches next 2 events + ;; + ;; :update-contact! + ;;TODO :update-chat! + (let [timestamp (datetime/timestamp)] + (def received-contact2 (assoc received-contact1 + :last-updated timestamp + :status "new status" + :name "new name")) + + (rf/dispatch [:contact-update-received {:from new-contact-public-key + :payload {:content {:profile {:profile-image "" + :status "new status" + :name "new name"}} + :timestamp timestamp}}])) + + (testing "it updates the contact and set the :last-updated key" + (is (= received-contact2 + (get @contacts new-contact-public-key))))) + + (testing ":hide-contact event" + + ;; :hide-contact event dispatches next 2 events + ;; + ;; :update-contact! + ;;TODO :account-update-keys + (rf/reg-event-db :account-update-keys (fn [db _] db)) + + (rf/dispatch [:hide-contact @contact]) + + (testing "it sets the pending? flag to true" + (is (= (assoc received-contact2 :pending? true) + (get @contacts new-contact-public-key))))) + + (testing ":add-contact-handler event - :add-contact" + + ;; :add-contact-handler event dispatches next 4 events + ;; + ;; :add-contact + ;; :status-im.contacts.events/add-new-contact + ;; :status-im.contacts.events/send-contact-request + ;;TODO :discoveries-send-portions + (rf/reg-event-db :discoveries-send-portions (fn [db _] db)) + + (rf/dispatch [:set :view-id nil]) + (rf/dispatch [:set :current-chat-id nil]) + (rf/dispatch [:set :contacts/new-identity new-contact-public-key]) + (rf/dispatch [:add-contact-handler]) + + (testing "it sets the pending? flag to false" + (is (= (assoc received-contact2 :pending? false) + (get @contacts new-contact-public-key)))) - (testing "it loads the 1-1 chat" - (is (= :chat @view-id))) + (testing "it loads the 1-1 chat" + (is (= :chat @view-id))) - (testing "it adds the new contact to the chat" - (is (= new-contact-public-key @current-chat-id)))) + (testing "it adds the new contact to the chat" + (is (= new-contact-public-key @current-chat-id)))) - (testing ":create-new-contact-group event" - - (def new-group-name "new group") - (rf/dispatch [:select-contact new-contact-public-key]) - (rf/dispatch [:select-contact "demo-bot"]) - (rf/dispatch [:select-contact "browse"]) - (rf/dispatch [:deselect-contact "browse"]) + (testing ":create-new-contact-group event" + + (def new-group-name "new group") + (rf/dispatch [:select-contact new-contact-public-key]) + (rf/dispatch [:select-contact "demo-bot"]) + (rf/dispatch [:select-contact "browse"]) + (rf/dispatch [:deselect-contact "browse"]) - (rf/dispatch [:create-new-contact-group new-group-name]) - - (rf/dispatch [:deselect-contact "demo-bot"]) - (rf/dispatch [:deselect-contact new-contact-public-key]) - (def new-group-id (->> @contact-groups - (vals) - (filter #(= (:name %) new-group-name)) - (first) - (:group-id))) - (def new-group {:group-id new-group-id - :name new-group-name - :order 2 - :timestamp 0 - :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"}]}) - (def groups-with-new-group {new-group-id new-group - "dapps" dapps-contact-group - (:group-id test-contact-group) test-contact-group}) - (def groups-with-new-group1 (update groups-with-new-group new-group-id assoc :name "new group name")) - (def groups-with-new-group2 (-> groups-with-new-group1 - (update new-group-id assoc :order 1) - (update "dapps" assoc :order 2))) + (rf/dispatch [:create-new-contact-group new-group-name]) + + (rf/dispatch [:deselect-contact "demo-bot"]) + (rf/dispatch [:deselect-contact new-contact-public-key]) + (def new-group-id (->> @contact-groups + (vals) + (filter #(= (:name %) new-group-name)) + (first) + (:group-id))) + (def new-group {:group-id new-group-id + :name new-group-name + :order 2 + :timestamp 0 + :contacts [{:identity new-contact-public-key} + {:identity "demo-bot"}]}) + (def groups-with-new-group {new-group-id new-group + "dapps" dapps-contact-group + (:group-id test-contact-group) test-contact-group}) + (def groups-with-new-group1 (update groups-with-new-group new-group-id assoc :name "new group name")) + (def groups-with-new-group2 (-> groups-with-new-group1 + (update new-group-id assoc :order 1) + (update "dapps" assoc :order 2))) - (rf/dispatch [:set-in [:group/contact-groups new-group-id :timestamp] 0]) + (rf/dispatch [:set-in [:group/contact-groups new-group-id :timestamp] 0]) - (is (= groups-with-new-group @contact-groups))) + (is (= groups-with-new-group @contact-groups))) - (testing ":set-contact-group-name event" + (testing ":set-contact-group-name event" - (rf/reg-event-db ::prepare-group-name - (fn [db _] (assoc db - :new-chat-name "new group name" - :group/contact-group-id new-group-id))) + (rf/reg-event-db ::prepare-group-name + (fn [db _] (assoc db + :new-chat-name "new group name" + :group/contact-group-id new-group-id))) - (rf/dispatch [::prepare-group-name]) - (rf/dispatch [:set-contact-group-name]) + (rf/dispatch [::prepare-group-name]) + (rf/dispatch [:set-contact-group-name]) - (is (= groups-with-new-group1 @contact-groups))) + (is (= groups-with-new-group1 @contact-groups))) - (testing ":save-contact-group-order event" + (testing ":save-contact-group-order event" - (rf/reg-event-db ::prepare-groups-order - (fn [db _] - (assoc db :group/groups-order - (->> (vals (:group/contact-groups db)) - (remove :pending?) - (sort-by :order >) - (map :group-id))))) + (rf/reg-event-db ::prepare-groups-order + (fn [db _] + (assoc db :group/groups-order + (->> (vals (:group/contact-groups db)) + (remove :pending?) + (sort-by :order >) + (map :group-id))))) - (rf/dispatch [::prepare-groups-order]) - (rf/dispatch [:change-contact-group-order 1 0]) - (rf/dispatch [:save-contact-group-order]) + (rf/dispatch [::prepare-groups-order]) + (rf/dispatch [:change-contact-group-order 1 0]) + (rf/dispatch [:save-contact-group-order]) - (is (= groups-with-new-group2 @contact-groups))) + (is (= groups-with-new-group2 @contact-groups))) - (testing ":add-selected-contacts-to-group event" + (testing ":add-selected-contacts-to-group event" - (rf/dispatch [:select-contact "browse"]) - (rf/dispatch [:add-selected-contacts-to-group]) - (rf/dispatch [:deselect-contact "browse"]) + (rf/dispatch [:select-contact "browse"]) + (rf/dispatch [:add-selected-contacts-to-group]) + (rf/dispatch [:deselect-contact "browse"]) - (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"} - {:identity "browse"}]) - @contact-groups))) + (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} + {:identity "demo-bot"} + {:identity "browse"}]) + @contact-groups))) - (testing ":remove-contact-from-group event" + (testing ":remove-contact-from-group event" - (rf/dispatch [:remove-contact-from-group "browse" new-group-id]) + (rf/dispatch [:remove-contact-from-group "browse" new-group-id]) - (is (= groups-with-new-group2 @contact-groups))) + (is (= groups-with-new-group2 @contact-groups))) - (testing ":add-contacts-to-group event" + (testing ":add-contacts-to-group event" - (rf/dispatch [:add-contacts-to-group new-group-id ["browse"]]) + (rf/dispatch [:add-contacts-to-group new-group-id ["browse"]]) - (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} - {:identity "demo-bot"} - {:identity "browse"}]) - @contact-groups)) + (is (= (update groups-with-new-group2 new-group-id assoc :contacts [{:identity new-contact-public-key} + {:identity "demo-bot"} + {:identity "browse"}]) + @contact-groups)) - (rf/dispatch [:remove-contact-from-group "browse" new-group-id])) + (rf/dispatch [:remove-contact-from-group "browse" new-group-id])) - (testing ":delete-contact-group event" + (testing ":delete-contact-group event" - (rf/dispatch [:delete-contact-group]) + (rf/dispatch [:delete-contact-group]) - (is (= (update groups-with-new-group2 new-group-id assoc :pending? true) - @contact-groups))))) + (is (= (update groups-with-new-group2 new-group-id assoc :pending? true) + @contact-groups))))) diff --git a/test/cljs/status_im/test/protocol/core.cljs b/test/cljs/status_im/test/protocol/core.cljs index 4822ad0f70..c202bcac94 100644 --- a/test/cljs/status_im/test/protocol/core.cljs +++ b/test/cljs/status_im/test/protocol/core.cljs @@ -3,102 +3,103 @@ async use-fixtures]] [cljs.nodejs :as nodejs] [cljs.core.async :as async] - [status-im.protocol.web3.utils :as web3.utils] + ;;[status-im.protocol.web3.utils :as web3.utils] [status-im.test.protocol.node :as node] [status-im.test.protocol.utils :as utils] - [status-im.protocol.core :as protocol])) + ;;[status-im.protocol.core :as protocol] + )) ;; NOTE(oskarth): All these tests are evaluated in NodeJS -(nodejs/enable-util-print!) +;; (nodejs/enable-util-print!) -(def rpc-url "http://localhost:8645") -(def Web3 (js/require "web3")) -(defn make-web3 [] - (Web3. (Web3.providers.HttpProvider. rpc-url))) +;; (def rpc-url "http://localhost:8645") +;; (def Web3 (js/require "web3")) +;; (defn make-web3 [] +;; (Web3. (Web3.providers.HttpProvider. rpc-url))) -(defn setup [] - (println "Setup...") +;; (defn setup [] +;; (println "Setup...") - ;; NOTE(oskarth): If status-go has already been built correctly, comment this out - (println "Preparing environment...") - (node/prepare-env!) +;; ;; NOTE(oskarth): If status-go has already been built correctly, comment this out +;; (println "Preparing environment...") +;; (node/prepare-env!) - (println "Start node...") - (node/start!) +;; (println "Start node...") +;; (node/start!) - (println "Setup done")) +;; (println "Setup done")) -(defn teardown [] - (println "Teardown done")) +;; (defn teardown [] +;; (println "Teardown done")) -(use-fixtures :once {:before setup :after teardown}) +;; (use-fixtures :once {:before setup :after teardown}) -(defn make-callback [identity done] - (fn [& args] - (is (contains? #{:sent :pending} (first args))) - (when (= (first args) :sent) - (protocol/reset-all-pending-messages!) - (protocol/stop-watching-all!) - (node/stop!) - (done) - (utils/exit!)))) +;; (defn make-callback [identity done] +;; (fn [& args] +;; (is (contains? #{:sent :pending} (first args))) +;; (when (= (first args) :sent) +;; (protocol/reset-all-pending-messages!) +;; (protocol/stop-watching-all!) +;; (node/stop!) +;; (done) +;; (utils/exit!)))) -(defn post-error-callback [identity] - (fn [& args] - (.log js/console (str :post-error " " identity "\n" args)))) +;; (defn post-error-callback [identity] +;; (fn [& args] +;; (.log js/console (str :post-error " " identity "\n" args)))) -(defn id-specific-config - [id {:keys [private public]} contacts done] - {:identity id - :callback (make-callback id done) - :profile-keypair {:public public - :private private} - :contacts contacts - :post-error-callback (post-error-callback id)}) +;; (defn id-specific-config +;; [id {:keys [private public]} contacts done] +;; {:identity id +;; :callback (make-callback id done) +;; :profile-keypair {:public public +;; :private private} +;; :contacts contacts +;; :post-error-callback (post-error-callback id)}) -(defn ensure-test-terminates! [timeout done] - (async/go (async/