status-react/src/status_im/chat/handlers.cljs
2017-05-02 18:49:44 +03:00

520 lines
20 KiB
Clojure

(ns status-im.chat.handlers
(:require-macros [cljs.core.async.macros :as am])
(:require [re-frame.core :refer [enrich after debug dispatch]]
[status-im.models.commands :as commands]
[clojure.string :as string]
[status-im.components.styles :refer [default-chat-color]]
[status-im.chat.models.suggestions :as suggestions]
[status-im.chat.constants :as chat-consts]
[status-im.protocol.core :as protocol]
[status-im.data-store.chats :as chats]
[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.constants :refer [text-content-type
content-type-command
content-type-command-request
default-number-of-messages
console-chat-id
wallet-chat-id]]
[status-im.utils.random :as random]
[status-im.chat.sign-up :as sign-up-service]
[status-im.navigation.handlers :as nav]
[status-im.utils.handlers :refer [register-handler] :as u]
[status-im.handlers.server :as server]
[status-im.utils.phone-number :refer [format-phone-number
valid-mobile-number?]]
[status-im.components.status :as status]
[status-im.utils.types :refer [json->clj]]
[status-im.chat.utils :refer [console? not-console? safe-trim]]
[status-im.utils.gfycat.core :refer [generate-gfy]]
status-im.chat.handlers.input
status-im.chat.handlers.commands
status-im.chat.handlers.animation
status-im.chat.handlers.requests
status-im.chat.handlers.unviewed-messages
status-im.chat.handlers.send-message
status-im.chat.handlers.receive-message
status-im.chat.handlers.faucet
[cljs.core.async :as a]
status-im.chat.handlers.webview-bridge
status-im.chat.handlers.console
[taoensso.timbre :as log]
[tailrecursion.priority-map :refer [priority-map-by]]))
(register-handler :set-layout-height
(fn [db [_ height]]
(assoc db :layout-height height)))
(register-handler :set-chat-ui-props
(fn [{:keys [current-chat-id] :as db} [_ kvs]]
(update-in db [:chat-ui-props current-chat-id] merge kvs)))
(register-handler :toggle-chat-ui-props
(fn [{:keys [current-chat-id chat-ui-props] :as db} [_ ui-element chat-id]]
(let [chat-id (or chat-id current-chat-id)]
(update-in db [:chat-ui-props chat-id ui-element] not))))
(register-handler :show-message-details
(u/side-effect!
(fn [_ [_ details]]
(dispatch [:set-chat-ui-props {:show-bottom-info? true
:show-emoji? false
:bottom-info details}]))))
(register-handler :load-more-messages
(fn [{:keys [current-chat-id loading-allowed] :as db} _]
(let [all-loaded? (get-in db [:chats current-chat-id :all-loaded?])]
(if loading-allowed
(do (am/go
(<! (a/timeout 400))
(dispatch [:set :loading-allowed true]))
(if all-loaded?
db
(let [messages-path [:chats current-chat-id :messages]
messages (get-in db messages-path)
chat-messages (filter #(= current-chat-id (:chat-id %)) messages)
new-messages (messages/get-by-chat-id current-chat-id (count chat-messages))
all-loaded? (> default-number-of-messages (count new-messages))]
(-> db
(assoc :loading-allowed false)
(update-in messages-path concat new-messages)
(assoc-in [:chats current-chat-id :all-loaded?] all-loaded?)))))
db))))
(defn set-message-shown
[db chat-id message-id]
(update-in db
[:chats chat-id :messages]
(fn [messages]
(map (fn [message]
(if (= message-id (:message-id message))
(assoc message :new? false)
message))
messages))))
(register-handler :set-message-shown
(fn [db [_ {:keys [chat-id message-id]}]]
(set-message-shown db chat-id message-id)))
(register-handler :cancel-command
(fn [{:keys [current-chat-id] :as db} _]
(-> db
(dissoc :canceled-command)
(assoc-in [:chats current-chat-id :command-input] {})
(update-in [:chats current-chat-id :input-text] safe-trim))))
(defn init-console-chat
([{:keys [chats current-account-id] :as db} existing-account?]
(let [new-chat sign-up-service/console-chat]
(if (chats console-chat-id)
db
(do
(dispatch [:add-contacts [sign-up-service/console-contact]])
(chats/save new-chat)
(contacts/save-all [sign-up-service/console-contact])
(when-not current-account-id
(sign-up-service/intro))
(when existing-account?
(sign-up-service/start-signup))
(-> db
(assoc :new-chat new-chat)
(update :chats assoc console-chat-id new-chat)
(assoc :current-chat-id console-chat-id)))))))
(register-handler :init-console-chat
(fn [db _]
(init-console-chat db false)))
(register-handler :account-generation-message
(u/side-effect!
(fn [_]
(when-not (messages/get-by-id chat-consts/passphrase-message-id)
(sign-up-service/account-generation-message)))))
(register-handler :move-to-internal-failure-message
(u/side-effect!
(fn [_]
(when-not (messages/get-by-id chat-consts/move-to-internal-failure-message-id)
(sign-up-service/move-to-internal-failure-message)))))
(register-handler :show-mnemonic
(u/side-effect!
(fn [_ [_ mnemonic]]
(let [crazy-math-message? (messages/get-by-id chat-consts/crazy-math-message-id)]
(sign-up-service/passphrase-messages mnemonic crazy-math-message?)))))
(defn- handle-sms [{body :body}]
(when-let [matches (re-matches #"(\d{4})" body)]
(dispatch [:sign-up-confirm (second matches)])))
(register-handler :sign-up
(after (fn [_ [_ phone-number]]
(dispatch [:account-update {:phone phone-number}])))
(fn [db [_ phone-number message-id]]
(sign-up-service/start-listening-confirmation-code-sms)
(let [formatted (format-phone-number phone-number)]
(-> db
(assoc :user-phone-number formatted)
(server/sign-up formatted
message-id
sign-up-service/on-sign-up-response)))))
(register-handler :start-listening-confirmation-code-sms
(fn [db [_ listener]]
(if-not (:confirmation-code-sms-listener db)
(assoc db :confirmation-code-sms-listener listener)
db)))
(register-handler :stop-listening-confirmation-code-sms
(fn [db]
(if (:confirmation-code-sms-listener db)
(sign-up-service/stop-listening-confirmation-code-sms db)
db)))
(register-handler :sign-up-confirm
(u/side-effect!
(fn [_ [_ confirmation-code message-id]]
(server/sign-up-confirm
confirmation-code
message-id
sign-up-service/on-send-code-response))))
(register-handler :set-signed-up
(u/side-effect!
(fn [_ [_ signed-up]]
(dispatch [:account-update {:signed-up? signed-up}]))))
(defn load-messages!
([db] (load-messages! db nil))
([{:keys [current-chat-id] :as db} _]
(let [messages (messages/get-by-chat-id current-chat-id)]
(doseq [{:keys [content] :as message} messages]
(when (and (:command content)
(not (:content content)))
;; todo rewrite it so that commands defined outside chat's context
;; (bots' commands in group chats and global commands in all chats)
;; could be rendered properly
(dispatch [:request-command-data (assoc message :chat-id current-chat-id)])))
(assoc db :messages messages))))
(defn init-chat
([db] (init-chat db nil))
([{:keys [messages current-chat-id] :as db} _]
(-> db
(assoc-in [:chats current-chat-id :messages] messages)
(dissoc :messages))))
(defn load-commands!
[{:keys [current-chat-id]}]
(dispatch [:load-commands! current-chat-id]))
(register-handler :init-chat
(after #(dispatch [:load-requests!]))
(-> load-messages!
((enrich init-chat))
((after load-commands!))))
(defn compare-chats
[{timesatmp1 :timestamp} {timestamp2 :timestamp}]
(compare timestamp2 timesatmp1))
(defn initialize-chats
[{:keys [loaded-chats account-creation? chats] :as db} _]
(let [chats' (if account-creation?
chats
(->> loaded-chats
(map (fn [{:keys [chat-id] :as chat}]
(let [last-message (messages/get-last-message chat-id)]
[chat-id (assoc chat :last-message last-message)])))
(into (priority-map-by compare-chats))))]
(-> db
(assoc :chats chats')
(dissoc :loaded-chats)
(init-console-chat true))))
(defn load-chats!
[{:keys [account-creation?] :as db} _]
(if account-creation?
db
(assoc db :loaded-chats (chats/get-all))))
(register-handler :initialize-chats
[(after #(dispatch [:load-unviewed-messages!]))
(after #(dispatch [:load-default-contacts!]))]
((enrich initialize-chats) load-chats!))
(register-handler :reload-chats
(fn [{:keys [chats] :as db} _]
(let [chats' (->> (chats/get-all)
(map (fn [{:keys [chat-id] :as chat}]
(let [last-message (messages/get-last-message chat-id)
prev-chat (get chats chat-id)
new-chat (assoc chat :last-message last-message)]
[chat-id (merge prev-chat new-chat)])))
(into (priority-map-by compare-chats)))]
(-> (assoc db :chats chats')
(init-console-chat true)))))
(defmethod nav/preload-data! :chat
[{:keys [current-chat-id] :as db} [_ _ id]]
(let [chat-id (or id current-chat-id)
messages (get-in db [:chats chat-id :messages])
command? (= :command (get-in db [:edit-mode chat-id]))
db' (-> db
(assoc :current-chat-id chat-id)
(assoc-in [:chats chat-id :was-opened?] true))
commands-loaded? (get-in db [:contacts chat-id :commands-loaded])
bot-url (get-in db [:contacts chat-id :bot-url])
was-opened? (get-in db [:chats chat-id :was-opened?])
call-init-command #(when (and (not was-opened?) bot-url)
(status/call-function!
{:chat-id chat-id
:function :init}))]
(dispatch [:load-requests! chat-id])
;; todo rewrite this. temporary fix for https://github.com/status-im/status-react/issues/607
#_(dispatch [:load-commands! chat-id])
(if-not commands-loaded?
(dispatch [:load-commands! chat-id call-init-command])
(do
(call-init-command)
(dispatch [:invoke-chat-loaded-callbacks chat-id])))
(if (and (seq messages)
(not= (count messages) 1))
db'
(-> db'
load-messages!
init-chat))))
(register-handler :add-chat-loaded-callback
(fn [db [_ chat-id callback]]
(log/debug "Add chat loaded callback: " chat-id callback)
(update-in db [::chat-loaded-callbacks chat-id] conj callback)))
(register-handler ::clear-chat-loaded-callbacks
(fn [db [_ chat-id]]
(log/debug "Clear chat loaded callback: " chat-id)
(assoc-in db [::chat-loaded-callbacks chat-id] nil)))
(register-handler :invoke-chat-loaded-callbacks
(u/side-effect!
(fn [db [_ chat-id]]
(log/debug "Invoking chat loaded callbacks: " chat-id)
(let [callbacks (get-in db [::chat-loaded-callbacks chat-id])]
(log/debug "Invoking chat loaded callbacks: " callbacks)
(doseq [callback callbacks]
(callback))
(dispatch [::clear-chat-loaded-callbacks chat-id])))))
(defn prepare-chat [{:keys [contacts]} chat-id chat]
(let [name (get-in contacts [chat-id :name])]
(merge {:chat-id chat-id
:name (or name (generate-gfy))
:color default-chat-color
:group-chat false
:is-active true
:timestamp (.getTime (js/Date.))
:contacts [{:identity chat-id}]}
chat)))
(defn add-new-chat
[db [_ chat-id chat]]
(assoc db :new-chat (prepare-chat db chat-id chat)))
(defn add-chat [{:keys [new-chat chats] :as db} [_ chat-id]]
(if-not (get chats chat-id)
(update db :chats assoc chat-id new-chat)
db))
(defn save-new-chat!
[{{:keys [chat-id] :as new-chat} :new-chat} _]
(when-not (chats/exists? chat-id)
(chats/save new-chat)))
(defn open-chat!
[_ [_ chat-id _ navigation-type]]
(dispatch [(or navigation-type :navigate-to) :chat chat-id]))
(register-handler ::start-chat!
(-> add-new-chat
((enrich add-chat))
((after save-new-chat!))
((after open-chat!))))
(register-handler :start-chat
(u/side-effect!
(fn [{:keys [chats current-public-key]}
[_ contact-id options navigation-type]]
(when-not (= current-public-key contact-id)
(if (chats contact-id)
(dispatch [(or navigation-type :navigate-to) :chat contact-id])
(dispatch [::start-chat! contact-id options navigation-type]))))))
(register-handler :add-chat
(-> add-new-chat
((enrich add-chat))
((after save-new-chat!))))
(defn update-chat!
[_ [_ {:keys [name] :as chat}]]
(let [chat' (if name chat (dissoc chat :name))]
(chats/save chat')))
(register-handler :update-chat!
(after update-chat!)
(fn [db [_ {:keys [chat-id name] :as chat}]]
(let [chat' (if name chat (dissoc chat :name))]
(if (get-in db [:chats chat-id])
(update-in db [:chats chat-id] merge chat')
db))))
(register-handler :upsert-chat!
(fn [db [_ {:keys [chat-id] :as opts}]]
(let [chat (if (chats/exists? chat-id)
(let [chat (chats/get-by-id chat-id)]
(assoc chat :timestamp (random/timestamp)))
(prepare-chat db chat-id opts))]
(chats/save chat)
(update-in db [:chats chat-id] merge chat))))
(defn remove-chat
[db [_ chat-id]]
(update db :chats dissoc chat-id))
(defn delete-messages!
[{:keys [current-chat-id]} [_ chat-id]]
(let [id (or chat-id current-chat-id)]
(messages/delete-by-chat-id id)))
(defn delete-chat!
[_ [_ chat-id]]
(let [{:keys [debug?]} (chats/get-by-id chat-id)]
(if debug?
(chats/delete chat-id)
(chats/set-inactive chat-id))))
(defn remove-pending-messages!
[_ [_ chat-id]]
(pending-messages/delete-all-by-chat-id chat-id))
(register-handler :leave-group-chat
;; todo oreder of operations tbd
(after (fn [_ _] (dispatch [:navigation-replace :chat-list])))
(u/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)}})))
(dispatch [:remove-chat current-chat-id]))))
(register-handler :remove-chat
(-> remove-chat
((after delete-messages!))
((after remove-pending-messages!))
((after delete-chat!))))
(defn send-seen!
[{:keys [web3 current-public-key chats contacts]}
[_ {:keys [from chat-id message-id]}]]
(when-not (get-in contacts [chat-id :dapp?])
(let [{:keys [group-chat public?]} (chats chat-id)]
(when-not public?
(protocol/send-seen! {:web3 web3
:message {:from current-public-key
:to from
:group-id (when group-chat chat-id)
:message-id message-id}})))))
(register-handler :send-seen!
[(after (fn [_ [_ {:keys [message-id]}]]
(messages/update {:message-id message-id
:message-status :seen})))
(after (fn [_ [_ {:keys [chat-id]}]]
(dispatch [:remove-unviewed-messages chat-id])))]
(u/side-effect! send-seen!))
(defn send-clock-value-request!
[{:keys [web3 current-public-key contacts]} [_ {:keys [message-id from]}]]
(when-not (get-in contacts [from :dapp?])
(protocol/send-clock-value-request!
{:web3 web3
:message {:from current-public-key
:to from
:message-id message-id}})))
(register-handler :send-clock-value-request! (u/side-effect! send-clock-value-request!))
(defn send-clock-value!
[{:keys [web3 current-public-key]} to message-id clock-value]
(when current-public-key
(protocol/send-clock-value! {:web3 web3
:message {:from current-public-key
:to to
:message-id message-id
:clock-value clock-value}})))
(register-handler :update-clock-value!
(after (fn [db [_ to i {:keys [message-id] :as message} last-clock-value]]
(let [clock-value (+ last-clock-value i 1)]
(messages/update (assoc message :clock-value clock-value))
(send-clock-value! db to message-id clock-value))))
(fn [db [_ _ i {:keys [message-id]} last-clock-value]]
(assoc-in db [:message-extras message-id :clock-value] (+ last-clock-value i 1))))
(register-handler :send-clock-value!
(u/side-effect!
(fn [db [_ to message-id]]
(let [{:keys [clock-value]} (messages/get-by-id message-id)]
(send-clock-value! db to message-id clock-value)))))
(register-handler :check-and-open-dapp!
(u/side-effect!
(fn [{:keys [current-chat-id global-commands contacts] :as db}]
(let [dapp-url (get-in db [:contacts current-chat-id :dapp-url])]
(when dapp-url
(am/go
(dispatch [:select-chat-input-command
(assoc (:browse global-commands) :prefill [dapp-url])])
(a/<! (a/timeout 100))
(dispatch [:send-current-message])))))))
(register-handler :update-group-message
(u/side-effect!
(fn [{:keys [current-public-key web3 chats]}
[_ {:keys [from]
{:keys [group-id keypair timestamp]} :payload}]]
(let [{:keys [private public]} keypair]
(let [is-active (chats/is-active? group-id)
chat {:chat-id group-id
:public-key public
:private-key private
:updated-at timestamp}]
(when (and (= from (get-in chats [group-id :group-admin]))
(or (not (chats/exists? group-id))
(chats/new-update? timestamp group-id)))
(dispatch [:update-chat! chat])
(when is-active
(protocol/start-watching-group!
{:web3 web3
:group-id group-id
:identity current-public-key
:keypair keypair
:callback #(dispatch [:incoming-message %1 %2])}))))))))
(register-handler :update-message-overhead!
(u/side-effect!
(fn [_ [_ chat-id network-status]]
(if (= network-status :offline)
(chats/inc-message-overhead chat-id)
(chats/reset-message-overhead chat-id)))))