status-react/src/status_im/chat/models.cljs

327 lines
12 KiB
Clojure

(ns status-im.chat.models
(:require [re-frame.core :as re-frame]
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im.transport.filters.core :as transport.filters]
[status-im.contact.core :as contact.core]
[status-im.waku.core :as waku]
[status-im.contact.db :as contact.db]
[status-im.data-store.chats :as chats-store]
[status-im.data-store.messages :as messages-store]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.i18n :as i18n]
[status-im.mailserver.core :as mailserver]
[status-im.transport.message.protocol :as transport.protocol]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.react :as react]
[status-im.ui.screens.navigation :as navigation]
[status-im.utils.clocks :as utils.clocks]
[status-im.utils.config :as config]
[status-im.utils.fx :as fx]
[status-im.utils.gfycat.core :as gfycat]
[status-im.utils.platform :as platform]
[status-im.utils.utils :as utils]
[taoensso.timbre :as log]))
(defn- get-chat [cofx chat-id]
(get-in cofx [:db :chats chat-id]))
(defn multi-user-chat?
([chat]
(:group-chat chat))
([cofx chat-id]
(multi-user-chat? (get-chat cofx chat-id))))
(def one-to-one-chat?
(complement multi-user-chat?))
(defn public-chat?
([chat]
(:public? chat))
([cofx chat-id]
(public-chat? (get-chat cofx chat-id))))
(defn active-chat?
([chat]
(:is-active chat))
([cofx chat-id]
(active-chat? (get-chat cofx chat-id))))
(defn group-chat?
([chat]
(and (multi-user-chat? chat)
(not (public-chat? chat))))
([cofx chat-id]
(group-chat? (get-chat cofx chat-id))))
(defn set-chat-ui-props
"Updates ui-props in active chat by merging provided kvs into them"
[{:keys [current-chat-id] :as db} kvs]
(update-in db [:chat-ui-props current-chat-id] merge kvs))
(defn toggle-chat-ui-prop
"Toggles chat ui prop in active chat"
[{:keys [current-chat-id] :as db} ui-element]
(update-in db [:chat-ui-props current-chat-id ui-element] not))
(fx/defn join-time-messages-checked
"The key :might-have-join-time-messages? in public chats signals that
the public chat is freshly (re)created and requests for messages to the
mailserver for the topic has not completed yet. Likewise, the key
:join-time-mail-request-id is associated a little bit after, to signal that
the request to mailserver was a success. When request is signalled complete
by mailserver, corresponding event :chat.ui/join-time-messages-checked
dissociates these two fileds via this function, thereby signalling that the
public chat is not fresh anymore."
[{:keys [db] :as cofx} chat-id]
(when (:might-have-join-time-messages? (get-chat cofx chat-id))
{:db (update-in db
[:chats chat-id]
dissoc
:join-time-mail-request-id
:might-have-join-time-messages?)}))
(defn- create-new-chat
[chat-id {:keys [db now]}]
(let [name (get-in db [:contacts/contacts chat-id :name])]
{:chat-id chat-id
:name (or name "")
:color (rand-nth colors/chat-colors)
:group-chat false
:is-active true
:timestamp now
:contacts #{chat-id}
:last-clock-value 0
:messages {}}))
(fx/defn ensure-chat
"Add chat to db and update"
[{:keys [db] :as cofx} {:keys [chat-id] :as chat-props}]
(let [chat (merge
(or (get (:chats db) chat-id)
(create-new-chat chat-id cofx))
chat-props)
new? (not (get-in db [:chats chat-id]))
public? (public-chat? chat)]
(fx/merge cofx
{:db (update-in db [:chats chat-id] merge chat)}
(when (and public? new?)
(transport.filters/load-chat chat-id)))))
(fx/defn upsert-chat
"Upsert chat when not deleted"
[{:keys [db] :as cofx} {:keys [chat-id] :as chat-props}]
(fx/merge cofx
(ensure-chat chat-props)
#(chats-store/save-chat % (get-in % [:db :chats chat-id]))))
(fx/defn handle-save-chat
{:events [::save-chat]}
[{:keys [db] :as cofx} chat-id]
(chats-store/save-chat cofx (get-in db [:chats chat-id])))
(fx/defn handle-mark-all-read-successful
{:events [::mark-all-read-successful]}
[{:keys [db] :as cofx} chat-id]
{:db (assoc-in db [:chats chat-id :unviewed-messages-count] 0)})
(fx/defn handle-mark-all-read
{:events [:chat.ui/mark-all-read-pressed
:chat.ui/mark-public-all-read]}
[{:keys [db] :as cofx} chat-id]
{::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "markAllRead")
:params [chat-id]
:on-success #(re-frame/dispatch [::mark-all-read-successful chat-id])}]})
(fx/defn add-public-chat
"Adds new public group chat to db"
[cofx topic]
(upsert-chat cofx
{:chat-id topic
:is-active true
:name topic
:group-chat true
:contacts #{}
:public? true
:might-have-join-time-messages? true
:unviewed-messages-count 0
:loaded-unviewed-messages-ids #{}}))
(fx/defn clear-history
"Clears history of the particular chat"
[{:keys [db] :as cofx} chat-id]
(let [{:keys [messages
last-message
deleted-at-clock-value]} (get-in db [:chats chat-id])
last-message-clock-value (or (:clock-value last-message)
deleted-at-clock-value
(utils.clocks/send 0))]
(fx/merge
cofx
{:db (update-in db [:chats chat-id] merge
{:messages {}
:message-list nil
:last-message nil
:unviewed-messages-count 0
:deleted-at-clock-value last-message-clock-value})}
(messages-store/delete-messages-by-chat-id chat-id)
#(chats-store/save-chat % (get-in % [:db :chats chat-id])))))
(fx/defn deactivate-chat
"Deactivate chat in db, no side effects"
[{:keys [db now] :as cofx} chat-id]
{:db (-> db
(assoc-in [:chats chat-id :is-active] false)
(assoc-in [:current-chat-id] nil))})
(fx/defn remove-chat
"Removes chat completely from app, producing all necessary effects for that"
[{:keys [db now] :as cofx} chat-id]
(fx/merge cofx
(mailserver/remove-gaps chat-id)
(mailserver/remove-range chat-id)
(deactivate-chat chat-id)
(clear-history chat-id)
(transport.filters/stop-listening chat-id)
(when (not (= (:view-id db) :home))
(navigation/navigate-to-cofx :home {}))))
(defn- unread-messages-number [chats]
(apply + (map :unviewed-messages-count chats)))
(fx/defn update-dock-badge-label
[cofx]
(let [chats (get-in cofx [:db :chats])
active-chats (filter :is-active (vals chats))
private-chats (filter (complement :public?) active-chats)
public-chats (filter :public? active-chats)
private-chats-unread-count (unread-messages-number private-chats)
public-chats-unread-count (unread-messages-number public-chats)
label (cond
(pos? private-chats-unread-count) private-chats-unread-count
(pos? public-chats-unread-count) "•"
:else nil)]
{:set-dock-badge-label label}))
(defn subtract-seen-messages
[old-count new-seen-messages-ids]
(max 0 (- old-count (count new-seen-messages-ids))))
(fx/defn update-chats-unviewed-messages-count
[{:keys [db] :as cofx} {:keys [chat-id loaded-unviewed-messages-ids]}]
(let [{:keys [loaded-unviewed-messages-ids unviewed-messages-count]}
(get-in db [:chats chat-id])]
{:db (update-in db [:chats chat-id] assoc
:unviewed-messages-count (subtract-seen-messages
unviewed-messages-count
loaded-unviewed-messages-ids)
:loaded-unviewed-messages-ids #{})}))
(fx/defn mark-messages-seen
"Marks all unviewed loaded messages as seen in particular chat"
[{:keys [db] :as cofx} chat-id]
(let [loaded-unviewed-ids (get-in db [:chats chat-id :loaded-unviewed-messages-ids])]
(when (seq loaded-unviewed-ids)
(fx/merge cofx
{:db (reduce (fn [acc message-id]
(assoc-in acc [:chats chat-id :messages
message-id :seen]
true))
db
loaded-unviewed-ids)}
(messages-store/mark-messages-seen chat-id loaded-unviewed-ids nil)
(update-chats-unviewed-messages-count {:chat-id chat-id})
(when platform/desktop?
(update-dock-badge-label))))))
(fx/defn offload-all-messages
{:events [::offload-all-messages]}
[{:keys [db] :as cofx}]
(when-let [current-chat-id (:current-chat-id db)]
{:db
(-> db
(dissoc :loaded-chat-id)
(update-in [:chats current-chat-id]
assoc
:all-loaded? false
:cursor nil
:messages-initialized? false
:messages {}
:message-list nil))}))
(fx/defn preload-chat-data
"Takes chat-id and coeffects map, returns effects necessary when navigating to chat"
[{:keys [db] :as cofx} chat-id]
(let [old-current-chat-id (:current-chat-id db)]
(fx/merge cofx
(when-not (= old-current-chat-id chat-id)
(offload-all-messages))
(fn [{:keys [db]}]
{:db (assoc db :current-chat-id chat-id)})
;; Group chat don't need this to load as all the loading of topics
;; happens on membership changes
(when-not (group-chat? cofx chat-id)
(transport.filters/load-chat chat-id))
(when platform/desktop?
(mark-messages-seen chat-id))
(when (and (one-to-one-chat? cofx chat-id) (not (contact.db/contact-exists? db chat-id)))
(contact.core/create-contact chat-id)))))
(fx/defn navigate-to-chat
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
[cofx chat-id]
(fx/merge cofx
(navigation/navigate-to-cofx :chat {})
(preload-chat-data chat-id)))
(fx/defn start-chat
"Start a chat, making sure it exists"
[{:keys [db] :as cofx} chat-id _]
;; don't allow to open chat with yourself
(when (not= (multiaccounts.model/current-public-key cofx) chat-id)
(fx/merge cofx
(upsert-chat {:chat-id chat-id
:is-active true})
(transport.filters/load-chat chat-id)
(navigate-to-chat chat-id))))
(fx/defn start-public-chat
"Starts a new public chat"
[cofx topic {:keys [dont-navigate?]}]
(if (active-chat? cofx topic)
(when-not dont-navigate?
(navigate-to-chat cofx topic))
(fx/merge cofx
(add-public-chat topic)
(transport.filters/load-chat topic)
#(when-not dont-navigate?
(navigate-to-chat % topic)))))
(fx/defn disable-chat-cooldown
"Turns off chat cooldown (protection against message spamming)"
[{:keys [db]}]
{:db (assoc db :chat/cooldown-enabled? false)})
;; effects
(re-frame/reg-fx
:show-cooldown-warning
(fn [_]
(utils/show-popup nil
(i18n/label :cooldown/warning-message)
#())))
(defn set-dock-badge-label [label]
"Sets dock badge label (OSX only for now).
Label must be a string. Pass nil or empty string to clear the label."
(.setDockBadgeLabel react/desktop-notification label))
(re-frame/reg-fx
:set-dock-badge-label
set-dock-badge-label)
(fx/defn show-profile
{:events [:chat.ui/show-profile]}
[cofx identity]
(fx/merge (assoc-in cofx [:db :contacts/identity] identity)
(contact.core/create-contact identity)
(navigation/navigate-to-cofx :profile nil)))