From eb8d0a8a7994b0ab17b49e4b306cd249850fbe08 Mon Sep 17 00:00:00 2001 From: janherich Date: Tue, 5 Dec 2017 14:03:25 +0100 Subject: [PATCH] Refactored message data-model and view --- src/status_im/chat/constants.cljs | 3 +- src/status_im/chat/events.cljs | 104 ++-- src/status_im/chat/events/commands.cljs | 68 +-- src/status_im/chat/events/input.cljs | 20 +- .../chat/events/receive_message.cljs | 99 ++-- src/status_im/chat/events/sign_up.cljs | 5 +- src/status_im/chat/handlers/send_message.cljs | 36 +- src/status_im/chat/models/input.cljs | 7 +- .../chat/models/unviewed_messages.cljs | 23 +- src/status_im/chat/screen.cljs | 268 ++++------ src/status_im/chat/sign_up.cljs | 34 +- src/status_im/chat/specs.cljs | 31 +- .../chat/styles/message/message.cljs | 53 +- src/status_im/chat/subs.cljs | 192 ++++--- src/status_im/chat/utils.cljs | 50 +- src/status_im/chat/views/input/input.cljs | 2 +- .../chat/views/message/datemark.cljs | 15 +- src/status_im/chat/views/message/message.cljs | 503 ++++++++---------- .../chat/views/message/request_message.cljs | 19 +- src/status_im/constants.cljs | 4 +- src/status_im/data_store/chats.cljs | 6 +- src/status_im/data_store/messages.cljs | 40 +- src/status_im/data_store/realm/messages.cljs | 14 +- .../realm/schemas/account/v19/chat.cljs | 39 ++ .../realm/schemas/account/v19/core.cljs | 32 +- .../realm/schemas/account/v19/message.cljs | 30 ++ .../impl/non_status_go_module.cljs | 2 +- src/status_im/protocol/handlers.cljs | 80 +-- .../ui/components/chat_icon/screen.cljs | 2 +- .../ui/screens/accounts/login/events.cljs | 17 +- .../screens/chats_list/views/inner_item.cljs | 179 +++---- src/status_im/ui/screens/contacts/subs.cljs | 13 +- src/status_im/ui/screens/db.cljs | 15 +- .../screens/group/chat_settings/events.cljs | 4 +- src/status_im/ui/screens/profile/db.cljs | 3 +- src/status_im/ui/screens/profile/events.cljs | 2 +- test/cljs/status_im/test/chat/events.cljs | 4 +- .../status_im/test/chat/models/input.cljs | 8 +- 38 files changed, 916 insertions(+), 1110 deletions(-) create mode 100644 src/status_im/data_store/realm/schemas/account/v19/chat.cljs create mode 100644 src/status_im/data_store/realm/schemas/account/v19/message.cljs diff --git a/src/status_im/chat/constants.cljs b/src/status_im/chat/constants.cljs index 422f7f538c..be0bc08c3f 100644 --- a/src/status_im/chat/constants.cljs +++ b/src/status_im/chat/constants.cljs @@ -2,8 +2,7 @@ (def command-char "/") (def spacing-char " ") -(def arg-wrapping-char "\"") -(def bot-char "@") +(def arg-wrapping-char "\"") (def input-height 56) (def max-input-height 66) diff --git a/src/status_im/chat/events.cljs b/src/status_im/chat/events.cljs index 01326e1e22..5639d23823 100644 --- a/src/status_im/chat/events.cljs +++ b/src/status_im/chat/events.cljs @@ -6,7 +6,7 @@ [status-im.chat.models :as model] [status-im.chat.models.unviewed-messages :as unviewed-messages-model] [status-im.chat.sign-up :as sign-up] - [status-im.chat.constants :as chat-const] + [status-im.chat.constants :as chat-const] [status-im.data-store.messages :as msg-store] [status-im.data-store.contacts :as contacts-store] [status-im.data-store.chats :as chats-store] @@ -40,16 +40,6 @@ (fn [cofx _] (assoc cofx :get-stored-messages msg-store/get-by-chat-id))) -(re-frame/reg-cofx - :get-last-stored-message - (fn [cofx _] - (assoc cofx :get-last-stored-message msg-store/get-last-message))) - -(re-frame/reg-cofx - :get-message-previews - (fn [cofx _] - (assoc cofx :message-previews (msg-store/get-previews)))) - (re-frame/reg-cofx :all-stored-chats (fn [cofx _] @@ -120,38 +110,25 @@ :show-emoji? false :bottom-info details}))) +(def index-messages (partial into {} (map (juxt :message-id identity)))) + (handlers/register-handler-fx :load-more-messages [(re-frame/inject-cofx :get-stored-messages)] - (fn [{{:keys [current-chat-id loading-allowed] :as db} :db - get-stored-messages :get-stored-messages} _] - (let [all-loaded? (get-in db [:chats current-chat-id :all-loaded?])] - (if (and loading-allowed (not all-loaded?)) - (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 (get-stored-messages current-chat-id (count chat-messages)) - all-loaded? (> const/default-number-of-messages (count new-messages))] - {:db (-> db - (assoc :loading-allowed false) - (update-in messages-path concat new-messages) - (assoc-in [:chats current-chat-id :all-loaded?] all-loaded?)) - ;; we permit loading more messages again after 400ms - :dispatch-later [{:ms 400 :dispatch [:set :loading-allowed true]}]}) - {:db db})))) + (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])) + new-messages (get-stored-messages current-chat-id loaded-count)] + {:db (-> db + (update-in [:chats current-chat-id :messages] merge (index-messages new-messages)) + (assoc-in [:chats current-chat-id :all-loaded?] + (> const/default-number-of-messages (count new-messages))))})))) (handlers/register-handler-db :set-message-shown [re-frame/trim-v] (fn [db [{:keys [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))))) + (update-in db [:chats chat-id :messages message-id] assoc :new? false))) (defn init-console-chat [{:keys [chats] :accounts/keys [current-account-id] :as db}] @@ -165,7 +142,7 @@ :save-all-contacts [sign-up/console-contact]} (not current-account-id) - (update :dispatch-n concat sign-up/intro-events)))) + (update :dispatch-n conj sign-up/intro-event)))) (handlers/register-handler-fx :init-console-chat @@ -175,37 +152,36 @@ (handlers/register-handler-fx :initialize-chats [(re-frame/inject-cofx :all-stored-chats) + (re-frame/inject-cofx :get-stored-messages) (re-frame/inject-cofx :stored-unviewed-messages) - (re-frame/inject-cofx :get-stored-unanswered-requests) - (re-frame/inject-cofx :get-last-stored-message) - (re-frame/inject-cofx :get-message-previews)] + (re-frame/inject-cofx :get-stored-unanswered-requests)] (fn [{:keys [db all-stored-chats stored-unanswered-requests - stored-unviewed-messages - get-last-stored-message - message-previews]} _] - (let [{:accounts/keys [account-creation?] :contacts/keys [contacts]} db - new-db (unviewed-messages-model/load-unviewed-messages db stored-unviewed-messages) - event [:load-default-contacts!]] + get-stored-messages + stored-unviewed-messages]} _] + (let [{:accounts/keys [account-creation?]} db + load-default-contacts-event [:load-default-contacts!]] (if account-creation? - {:db new-db - :dispatch event} - (let [chat->message-id->request (reduce (fn [acc {:keys [chat-id message-id] :as request}] + {:db db + :dispatch load-default-contacts-event} + (let [chat->unviewed-messages (unviewed-messages-model/index-unviewed-messages stored-unviewed-messages) + chat->message-id->request (reduce (fn [acc {:keys [chat-id message-id] :as request}] (assoc-in acc [chat-id message-id] request)) {} stored-unanswered-requests) - chats (->> all-stored-chats - (map (fn [{:keys [chat-id] :as chat}] - [chat-id (assoc chat - :last-message (get-last-stored-message chat-id) - :requests (get chat->message-id->request chat-id))])) - (into {}))] - (-> new-db - (assoc-in [:message-data :preview] message-previews) + chats (reduce (fn [acc {:keys [chat-id] :as chat}] + (assoc acc chat-id + (assoc chat + :unviewed-messages (get chat->unviewed-messages chat-id) + :requests (get chat->message-id->request chat-id) + :messages (index-messages (get-stored-messages chat-id))))) + {} + all-stored-chats)] + (-> db (assoc :chats chats) init-console-chat - (update :dispatch-n conj event))))))) + (update :dispatch-n conj load-default-contacts-event))))))) (handlers/register-handler-fx :send-seen! @@ -214,7 +190,9 @@ (let [{:keys [web3 current-public-key chats] :contacts/keys [contacts]} db {:keys [group-chat public?]} (get chats chat-id)] - (cond-> {:db (unviewed-messages-model/remove-unviewed-messages db chat-id) + (cond-> {:db (-> db + (unviewed-messages-model/remove-unviewed-message chat-id message-id) + (assoc-in [:chats chat-id :messages message-id :message-status] :seen)) :update-message {:message-id message-id :message-status :seen}} (and (not (get-in contacts [chat-id] :dapp?)) @@ -256,7 +234,7 @@ (defn preload-chat-data "Takes coeffects map and chat-id, returns effects necessary when navigating to chat" - [{:keys [db get-stored-messages]} chat-id] + [{:keys [db]} chat-id] (let [messages (get-in db [:chats chat-id :messages]) chat-loaded-event (get-in db [:chats chat-id :chat-loaded-event]) jail-loaded? (get-in db [:contacts/contacts chat-id :jail-loaded?])] @@ -266,9 +244,6 @@ (model/set-chat-ui-props {:validation-messages nil}) (update-in [:chats chat-id] dissoc :chat-loaded-event))} - (empty? messages) - (assoc-in [:db :chats chat-id :messages] (get-stored-messages chat-id)) - chat-loaded-event (assoc :dispatch chat-loaded-event)))) @@ -301,14 +276,13 @@ (handlers/register-handler-fx :navigate-to-chat - [(re-frame/inject-cofx :get-stored-messages) re-frame/trim-v] + [re-frame/trim-v] (fn [cofx [chat-id {:keys [navigation-replace?]}]] (navigate-to-chat cofx chat-id navigation-replace?))) (handlers/register-handler-fx :start-chat - [(re-frame/inject-cofx :get-stored-messages) - re-frame/trim-v] + [re-frame/trim-v] (fn [{:keys [db] :as cofx} [contact-id {:keys [navigation-replace?]}]] (when (not= (:current-public-key db) contact-id) ; don't allow to open chat with yourself (if (get (:chats db) contact-id) diff --git a/src/status_im/chat/events/commands.cljs b/src/status_im/chat/events/commands.cljs index 9643cf79ff..3c404b9df5 100644 --- a/src/status_im/chat/events/commands.cljs +++ b/src/status_im/chat/events/commands.cljs @@ -3,7 +3,7 @@ [clojure.string :as str] [re-frame.core :as re-frame] [taoensso.timbre :as log] - [status-im.utils.handlers :as handlers] + [status-im.utils.handlers :as handlers] [status-im.i18n :as i18n] [status-im.utils.platform :as platform])) @@ -11,13 +11,12 @@ (defn- generate-context "Generates context for jail call" - [{:keys [chats] :accounts/keys [current-account-id]} chat-id to group-id] + [current-account-id chat-id to group-id] (merge {:platform platform/platform :from current-account-id :to to :chat {:chat-id chat-id - :group-chat (or (get-in chats [chat-id :group-chat]) - (not (nil? group-id)))}} + :group-chat (not (nil? group-id))}} i18n/delimeters)) (defn request-command-message-data @@ -25,52 +24,56 @@ [db {{command-name :command content-command-name :content-command - :keys [content-command-scope-bitmask scope-bitmask params type bot]} :content - :keys [chat-id jail-id group-id] :as message} - data-type] - (let [{:keys [chats] - :accounts/keys [current-account-id] + :keys [content-command-scope-bitmask bot scope-bitmask params type]} :content + :keys [chat-id group-id jail-id] :as message} + {:keys [data-type proceed-event-creator cache-data?] :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 (get-in contacts [jail-id :jail-loaded?]) (let [path [(if (= :response (keyword type)) :responses :commands) [jail-command-name - (or scope-bitmask content-command-scope-bitmask)] + (or content-command-scope-bitmask scope-bitmask)] data-type] to (get-in contacts [chat-id :address]) jail-params {:parameters params - :context (generate-context db chat-id to group-id)}] + :context (generate-context current-account-id chat-id to group-id)}] {:call-jail {:jail-id jail-id :path path :params jail-params :callback-events-creator (fn [jail-response] [[::jail-command-data-response - jail-response message data-type]])}}) + jail-response message opts]])}}) {:db (update-in db [:contacts/contacts jail-id :jail-loaded-events] - conj [:request-command-message-data message data-type])}))) + conj [:request-command-message-data message opts])}))) ;;;; Handlers (handlers/register-handler-fx ::jail-command-data-response [re-frame/trim-v] - (fn [{:keys [db]} [{{:keys [returned]} :result} {:keys [message-id on-requested]} data-type]] - (cond-> {} - returned - (assoc :db (assoc-in db [:message-data data-type message-id] returned)) - (and returned - (= :preview data-type)) - (assoc :update-message {:message-id message-id - :preview (prn-str returned)}) - on-requested - (assoc :dispatch (on-requested returned))))) + (fn [{:keys [db]} [{{:keys [returned]} :result} + {:keys [message-id chat-id]} + {:keys [data-type proceed-event-creator cache-data?]}]] + (let [existing-message (get-in db [:chats chat-id :messages message-id])] + (cond-> {} + + (and cache-data? existing-message returned) + (as-> fx + (let [updated-message (assoc-in existing-message [:content data-type] returned)] + (assoc fx + :db (assoc-in db [:chats chat-id :messages message-id] updated-message) + :update-message (select-keys updated-message [:message-id :content])))) + + proceed-event-creator + (assoc :dispatch (proceed-event-creator returned)))))) (handlers/register-handler-fx :request-command-message-data [re-frame/trim-v (re-frame/inject-cofx :get-local-storage-data)] - (fn [{:keys [db]} [message data-type]] - (request-command-message-data db message data-type))) + (fn [{:keys [db]} [message opts]] + (request-command-message-data db message opts))) (handlers/register-handler-fx :execute-command-immediately @@ -82,18 +85,3 @@ [:read-external-storage] #(re-frame/dispatch [:initialize-geth])]} (log/debug "ignoring command: " command-name)))) - -(handlers/register-handler-fx - :request-command-preview - [re-frame/trim-v (re-frame/inject-cofx :get-stored-message)] - (fn [{:keys [db get-stored-message]} [{:keys [message-id] :as message}]] - (let [previews (get-in db [:message-data :preview])] - (when-not (contains? previews message-id) - (let [{serialized-preview :preview} (get-stored-message message-id)] - ;; if preview is already cached in db, do not request it from jail - ;; and write it directly to message-data path - (if serialized-preview - {:db (assoc-in db - [:message-data :preview message-id] - (reader/read-string serialized-preview))} - (request-command-message-data db message :preview))))))) diff --git a/src/status_im/chat/events/input.cljs b/src/status_im/chat/events/input.cljs index 522bd60705..116a7606f4 100644 --- a/src/status_im/chat/events/input.cljs +++ b/src/status_im/chat/events/input.cljs @@ -270,10 +270,11 @@ :content {:command (:name command) :scope-bitmask (:scope-bitmask command) :params params - :type (:type command)} - :on-requested (fn [jail-response] - (event-after-creator command-message jail-response))}] - (commands-events/request-command-message-data db request-data data-type))) + :type (:type command)}}] + (commands-events/request-command-message-data db request-data + {:data-type data-type + :proceed-event-creator (partial event-after-creator + command-message)}))) (defn proceed-command "Proceed with command processing by setting up execution chain of events: @@ -429,10 +430,13 @@ (animation-events/choose-predefined-expandable-height :result-box :max)) ::dismiss-keyboard nil} ;; regular command message, we need to fetch preview before sending the command message - (request-command-data db (merge params-template - {:data-type :preview - :event-after-creator (fn [command-message _] - [::send-command command-message])}))))) + (request-command-data + db + (merge params-template + {:data-type :preview + :event-after-creator (fn [command-message returned] + [::send-command (assoc-in command-message + [:command :preview] returned)])}))))) (handlers/register-handler-fx :send-current-message diff --git a/src/status_im/chat/events/receive_message.cljs b/src/status_im/chat/events/receive_message.cljs index 58a3d4d698..639e18368c 100644 --- a/src/status_im/chat/events/receive_message.cljs +++ b/src/status_im/chat/events/receive_message.cljs @@ -9,6 +9,7 @@ [status-im.chat.models :as model] [status-im.chat.models.commands :as commands-model] [status-im.chat.models.unviewed-messages :as unviewed-messages-model] + [status-im.chat.events.commands :as commands-events] [status-im.chat.events.requests :as requests-events] [status-im.data-store.chats :as chat-store] [status-im.data-store.messages :as msg-store])) @@ -43,35 +44,36 @@ contacts)] (:ref (get available-commands-responses response-name)))) +(defn- add-message-to-db + [db {:keys [message-id] :as message} chat-id] + (-> db + (chat-utils/add-message-to-db chat-id chat-id message (:new? message)) + (unviewed-messages-model/add-unviewed-message chat-id message-id))) + (defn add-message - [{:keys [db message-exists? get-last-stored-message pop-up-chat? - get-last-clock-value now random-id] :as cofx} - {:keys [from group-id chat-id content-type content - message-id timestamp clock-value] + [{:keys [db message-exists? pop-up-chat? get-last-clock-value now] :as cofx} + {:keys [from group-id chat-id content-type content message-id timestamp clock-value] :as message :or {clock-value 0}}] - (let [{:keys [access-scope->commands-responses] :contacts/keys [contacts]} db - chat-identifier (or group-id chat-id from) - current-account (get-current-account db)] + (let [{:keys [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)] ;; proceed with adding message if message is not already stored in realm, ;; it's not from current user (outgoing message) and it's for relevant chat ;; (either current active chat or new chat not existing yet) - (if (and (not (message-exists? message-id)) - (not= from (:public-key current-account)) - (pop-up-chat? chat-identifier)) - (let [group-chat? (not (nil? group-id)) - chat-exists? (get-in db [:chats chat-identifier]) - fx (if chat-exists? + (when (and (not (message-exists? message-id)) + (not= from public-key) + (pop-up-chat? chat-identifier)) + (let [fx (if (get-in db [:chats chat-identifier]) (model/upsert-chat cofx {:chat-id chat-identifier - :group-chat group-chat?}) + :group-chat (boolean group-id)}) (model/add-chat cofx chat-identifier)) command-request? (= content-type const/content-type-command-request) command (:command content) - enriched-message (cond-> (assoc (chat-utils/check-author-direction - (get-last-stored-message chat-identifier) - message) - :chat-id chat-identifier - :timestamp (or timestamp now) + enriched-message (cond-> (assoc message + :chat-id chat-identifier + :timestamp (or timestamp now) + :show? true :clock-value (clocks/receive clock-value (get-last-clock-value chat-identifier))) @@ -81,49 +83,50 @@ current-account (get-in fx [:db :chats chat-identifier]) contacts - command))) - update-db-fx #(-> % - (chat-utils/add-message-to-db chat-identifier chat-identifier enriched-message - (:new? enriched-message)) - (unviewed-messages-model/add-unviewed-message chat-identifier message-id) - (assoc-in [:chats chat-identifier :last-message] enriched-message))] + command)))] (cond-> (-> fx - (update :db update-db-fx) + (update :db add-message-to-db enriched-message chat-identifier) (assoc :save-message (dissoc enriched-message :new?))) - - command - (update :dispatch-n concat [[:request-command-message-data enriched-message :short-preview] - [:request-command-preview enriched-message]]) - command-request? - (requests-events/add-request chat-identifier enriched-message))) - {:db db}))) + (requests-events/add-request chat-identifier enriched-message)))))) (def ^:private receive-interceptors - [(re-frame/inject-cofx :message-exists?) (re-frame/inject-cofx :get-last-stored-message) - (re-frame/inject-cofx :pop-up-chat?) (re-frame/inject-cofx :get-last-clock-value) - (re-frame/inject-cofx :random-id) (re-frame/inject-cofx :get-stored-chat) re-frame/trim-v]) + [(re-frame/inject-cofx :message-exists?) (re-frame/inject-cofx :pop-up-chat?) + (re-frame/inject-cofx :get-last-clock-value) (re-frame/inject-cofx :get-stored-chat) + re-frame/trim-v]) +;; we need this internal event without jail checking, otherwise no response for the jail +;; call to generate preview would result to infinite loop of `:received-message` events (handlers/register-handler-fx - :received-protocol-message! - receive-interceptors - (fn [cofx [{:keys [from to payload]}]] - (add-message cofx (merge payload - {:from from - :to to - :chat-id from})))) - -(handlers/register-handler-fx - :received-message + ::received-message receive-interceptors (fn [cofx [message]] (add-message cofx message))) +(handlers/register-handler-fx + :received-message + receive-interceptors + (fn [{:keys [db] :as cofx} [{:keys [content] :as message}]] + (if (:command content) + ;; we are dealing with received command message, we can't add it right away, + ;; we first need to fetch preview and add it only after we already have the preview. + ;; note that `request-command-message-data` implicitly wait till jail is ready and + ;; call is made only after that + (commands-events/request-command-message-data + db message + {:data-type :preview + :proceed-event-creator (fn [preview] + [::received-message + (assoc-in message [:content :preview] preview)])}) + ;; regular non command message, we can add it right away + (add-message cofx message)))) + +;; TODO janherich: get rid of this special case once they hacky app start-up sequence is refactored (handlers/register-handler-fx :received-message-when-commands-loaded receive-interceptors - (fn [{:keys [db] :as cofx} [chat-id message]] + (fn [{:keys [db] :as cofx} [{:keys [chat-id] :as message}]] (if (and (:status-node-started? db) (get-in db [:contacts/contacts chat-id :jail-loaded?])) (add-message cofx message) - {:dispatch-later [{:ms 400 :dispatch [:received-message-when-commands-loaded chat-id message]}]}))) + {:dispatch-later [{:ms 400 :dispatch [:received-message-when-commands-loaded message]}]}))) diff --git a/src/status_im/chat/events/sign_up.cljs b/src/status_im/chat/events/sign_up.cljs index 36b612e709..036a8e8537 100644 --- a/src/status_im/chat/events/sign_up.cljs +++ b/src/status_im/chat/events/sign_up.cljs @@ -45,7 +45,7 @@ (defn- message-seen [{:keys [db] :as fx} message-id] (-> fx - (assoc-in [:db :message-data :statuses message-id :status] :seen) + (assoc-in [:db :chats const/console-chat-id :messages message-id :message-status] :seen) (assoc :update-message {:message-id message-id :message-status :seen}))) @@ -90,7 +90,8 @@ (message-seen message-id)))) (defn- extract-last-phone-number [chats] - (let [phone-message (->> (get-in chats ["console" :messages]) + (let [phone-message (->> (get-in chats [const/console-chat-id :messages]) + (map second) (some (fn [{:keys [type content] :as message}] (when (and (= type :response) (= (:command content) "phone")) diff --git a/src/status_im/chat/handlers/send_message.cljs b/src/status_im/chat/handlers/send_message.cljs index 572ef66504..5ce23f8c1f 100644 --- a/src/status_im/chat/handlers/send_message.cljs +++ b/src/status_im/chat/handlers/send_message.cljs @@ -40,6 +40,8 @@ :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 @@ -90,12 +92,11 @@ hidden-params (->> (:params command) (filter :hidden) (map :name)) - command' (->> (prepare-command current-public-key chat-id clock-value request content) - (cu/check-author-direction db chat-id))] + command' (prepare-command current-public-key chat-id clock-value request content)] (dispatch [:update-message-overhead! chat-id network-status]) (dispatch [:set-chat-ui-props {:sending-in-progress? false}]) (dispatch [::send-command! add-to-chat-id (assoc params :command command') hidden-params]) - (when (cu/console? chat-id) + (when (= console-chat-id chat-id) (dispatch [:console-respond-command params])))))) (register-handler ::send-command! @@ -104,7 +105,8 @@ (dispatch [::add-command add-to-chat-id params]) (dispatch [::save-command! add-to-chat-id params hidden-params]) (dispatch [::dispatch-responded-requests! params]) - (dispatch [::send-command-protocol! params])))) + (dispatch [::send-command-protocol! (update-in params [:command :content] + dissoc :preview :short-preview)])))) (register-handler ::add-command (after (fn [_ [_ _ {:keys [handler]}]] @@ -115,11 +117,9 @@ (register-handler ::save-command! (u/side-effect! (fn [db [_ chat-id {:keys [command]} hidden-params]] - (let [preview (get-in db [:message-data :preview (:message-id command)]) - command (cond-> (-> command + (let [command (cond-> (-> command (update-in [:content :params] #(apply dissoc % hidden-params)) - (dissoc :to-message :has-handler :raw-input)) - preview (assoc :preview (pr-str preview)))] + (dissoc :to-message :has-handler :raw-input)))] (dispatch [:upsert-chat! {:chat-id chat-id}]) (messages/save chat-id command))))) @@ -168,17 +168,15 @@ (fn [{:keys [network-status] :as db} [_ {:keys [chat-id identity message] :as params}]] (let [{:keys [group-chat public?]} (get-in db [:chats chat-id]) clock-value (messages/get-last-clock-value chat-id) - message' (cu/check-author-direction - db chat-id - {:message-id (random/id) - :chat-id chat-id - :content message - :from identity - :content-type text-content-type - :outgoing true - :timestamp (datetime/now-ms) - :clock-value (clocks/send clock-value) - :show? true}) + message' {:message-id (random/id) + :chat-id chat-id + :content message + :from identity + :content-type text-content-type + :outgoing true + :timestamp (datetime/now-ms) + :clock-value (clocks/send clock-value) + :show? true} message'' (cond-> message' (and group-chat public?) (assoc :group-id chat-id :message-type :public-group-user-message) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index b30e200303..d527d12933 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -24,16 +24,13 @@ original))))) (defn text-ends-with-space? [text] - (and (not (nil? text)) - (str/ends-with? text const/spacing-char))) + (and text (str/ends-with? text const/spacing-char))) (defn starts-as-command? "Returns true if `text` may be treated as a command. To make sure that text is command we need to use `possible-chat-actions` function." [text] - (and (not (nil? text)) - (or (str/starts-with? text const/bot-char) - (str/starts-with? text const/command-char)))) + (and text (str/starts-with? text const/command-char))) (defn split-command-args "Returns a list of command's arguments including the command's name. diff --git a/src/status_im/chat/models/unviewed_messages.cljs b/src/status_im/chat/models/unviewed_messages.cljs index 49380f7616..56bf1c12b2 100644 --- a/src/status_im/chat/models/unviewed_messages.cljs +++ b/src/status_im/chat/models/unviewed_messages.cljs @@ -1,18 +1,13 @@ (ns status-im.chat.models.unviewed-messages) -(defn load-unviewed-messages [db raw-unviewed-messages] - (assoc db :unviewed-messages - (->> raw-unviewed-messages - (group-by :chat-id) - (map (fn [[id messages]] - [id {:messages-ids (map :message-id messages) - :count (count messages)}])) - (into {})))) +(defn index-unviewed-messages [unviewed-messages] + (into {} + (map (fn [[chat-id messages]] + [chat-id (into #{} (map :message-id) messages)])) + (group-by :chat-id unviewed-messages))) -(defn add-unviewed-message [db chat-id message-id] - (-> db - (update-in [:unviewed-messages chat-id :messages-ids] conj message-id) - (update-in [:unviewed-messages chat-id :count] inc))) +(defn add-unviewed-message [db chat-id message-id] + (update-in db [:chats chat-id :unviewed-messages] (fnil conj #{}) message-id)) -(defn remove-unviewed-messages [db chat-id] - (update db :unviewed-messages dissoc chat-id)) +(defn remove-unviewed-message [db chat-id message-id] + (update-in db [:chats chat-id :unviewed-messages] disj message-id)) diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs index 72b4e3c74d..d0ff2c26f1 100644 --- a/src/status_im/chat/screen.cljs +++ b/src/status_im/chat/screen.cljs @@ -1,198 +1,110 @@ (ns status-im.chat.screen - (:require-macros [status-im.utils.views :refer [defview]]) - (:require [re-frame.core :refer [subscribe dispatch]] - [status-im.ui.components.react :refer [view - animated-view - text - modal - touchable-highlight - list-view - list-item]] - [status-im.ui.components.icons.vector-icons :as vi] - [status-im.ui.components.status-bar :refer [status-bar]] - [status-im.ui.components.chat-icon.screen :refer [chat-icon-view-action - chat-icon-view-menu-item]] - [status-im.chat.styles.screen :as st] - [status-im.utils.listview :refer [to-datasource-inverted]] - [status-im.utils.utils :refer [truncate-str]] + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [re-frame.core :as re-frame] + [status-im.ui.components.react :as react] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.components.status-bar :as status-bar] + [status-im.ui.components.chat-icon.screen :as chat-icon-screen] + [status-im.chat.styles.screen :as style] + [status-im.utils.listview :as listview] [status-im.utils.datetime :as time] - [status-im.utils.platform :as platform :refer [platform-specific]] - [status-im.ui.components.invertible-scroll-view :refer [invertible-scroll-view]] + [status-im.utils.platform :as platform] + [status-im.ui.components.invertible-scroll-view :as scroll-view] [status-im.ui.components.toolbar.view :as toolbar] - [status-im.chat.views.toolbar-content :refer [toolbar-content-view]] - [status-im.chat.views.message.message :refer [chat-message]] - [status-im.chat.views.message.datemark :refer [chat-datemark]] + [status-im.chat.views.toolbar-content :as toolbar-content] + [status-im.chat.views.message.message :as message] + [status-im.chat.views.message.datemark :as message-datemark] [status-im.chat.views.input.input :as input] - [status-im.chat.views.actions :refer [actions-view]] - [status-im.chat.views.bottom-info :refer [bottom-info-view]] - [status-im.chat.constants :as chat-const] - [status-im.i18n :refer [label label-pluralize]] + [status-im.chat.views.actions :as actions] + [status-im.chat.views.bottom-info :as bottom-info] + [status-im.i18n :as i18n] [status-im.ui.components.animation :as anim] - [status-im.ui.components.sync-state.offline :refer [offline-view]] - [status-im.constants :refer [content-type-status]] - [taoensso.timbre :as log] - [clojure.string :as str])) - -(defn contacts-by-identity [contacts] - (->> contacts - (map (fn [{:keys [identity] :as contact}] - [identity contact])) - (into {}))) - -(defn add-message-color [{:keys [from] :as message} contact-by-identity] - (if (= "system" from) - (assoc message :text-color :#4A5258 - :background-color :#D3EEEF) - (let [{:keys [text-color background-color]} (get contact-by-identity from)] - (assoc message :text-color text-color - :background-color background-color)))) + [status-im.ui.components.sync-state.offline :as offline] + [clojure.string :as string])) (defview chat-icon [] - [chat-id [:chat :chat-id] - group-chat [:chat :group-chat] - name [:chat :name] - color [:chat :color]] - ;; TODO stub data ('online' property) - [chat-icon-view-action chat-id group-chat name color true]) + (letsubs [{:keys [chat-id group-chat name color]} [:get-current-chat]] + [chat-icon-screen/chat-icon-view-action chat-id group-chat name color true])) -(defn typing [member] - [view st/typing-view - [view st/typing-background - [text {:style st/typing-text - :font :default} - (str member " " (label :t/is-typing))]]]) +(defn- toolbar-action [show-actions?] + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:set-chat-ui-props {:show-actions? (not show-actions?)}]) + :accessibility-label :chat-menu} + [react/view style/action + (if show-actions? + [vector-icons/icon :icons/dropdown-up] + [chat-icon])]]) -(defn typing-all [] - [view st/typing-all - ;; TODO stub data - (for [member ["Geoff" "Justas"]] - ^{:key member} [typing member])]) +(defview add-contact-bar [] + (letsubs [chat-id [:get-current-chat-id] + pending-contact? [:current-contact :pending?]] + (when pending-contact? + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:add-pending-contact chat-id])} + [react/view style/add-contact + [react/text {:style style/add-contact-text} + (i18n/label :t/add-to-contacts)]]]))) + +(defview chat-toolbar [] + (letsubs [show-actions? [:get-current-chat-ui-prop :show-actions?] + accounts [:get-accounts] + creating? [:get :accounts/creating-account?]] + [react/view + [status-bar/status-bar] + [toolbar/toolbar {:show-sync-bar? true} + (when-not (or show-actions? creating?) + (if (empty? accounts) + [toolbar/nav-clear-text (i18n/label :t/recover) + #(re-frame/dispatch [:navigate-to-modal :recover-modal])] + toolbar/default-nav-back)) + [toolbar-content/toolbar-content-view] + [toolbar-action show-actions?]] + [add-contact-bar]])) (defmulti message-row (fn [{{:keys [type]} :row}] type)) (defmethod message-row :datemark [{{:keys [value]} :row}] - (list-item [chat-datemark value])) + (react/list-item [message-datemark/chat-datemark value])) (defmethod message-row :default - [{:keys [contact-by-identity group-chat messages-count row index last-outgoing?]}] - (let [message (-> row - (add-message-color contact-by-identity) - (assoc :group-chat group-chat) - (assoc :messages-count messages-count) - (assoc :index index) - (assoc :last-message (= (js/parseInt index) (dec messages-count))) - (assoc :last-outgoing? last-outgoing?))] - (list-item [chat-message message]))) - -(defn toolbar-action [] - (let [show-actions (subscribe [:get-current-chat-ui-prop :show-actions?])] - (fn [] - (let [show-actions @show-actions] - [touchable-highlight - {:on-press #(dispatch [:set-chat-ui-props {:show-actions? (not show-actions)}]) - :accessibility-label :chat-menu} - [view st/action - (if show-actions - [vi/icon :icons/dropdown-up] - [chat-icon])]])))) - -(defview add-contact-bar [] - [chat-id [:get :current-chat-id] - pending-contact? [:current-contact :pending?]] - (when pending-contact? - [touchable-highlight - {:on-press #(dispatch [:add-pending-contact chat-id])} - [view st/add-contact - [text {:style st/add-contact-text} - (label :t/add-to-contacts)]]])) - -(defview chat-toolbar [] - [show-actions? [:get-current-chat-ui-prop :show-actions?] - accounts [:get-accounts] - creating? [:get :accounts/creating-account?]] - [view - [status-bar] - [toolbar/toolbar {:show-sync-bar? true} - (when-not (or show-actions? creating?) - (if (empty? accounts) - [toolbar/nav-clear-text (label :t/recover) #(dispatch [:navigate-to-modal :recover-modal])] - toolbar/default-nav-back)) - [toolbar-content-view] - [toolbar-action]] - [add-contact-bar]]) - -(defn get-intro-status-message [all-messages] - (let [{:keys [timestamp content-type]} (last all-messages)] - (when (not= content-type content-type-status) - {:message-id chat-const/intro-status-message-id - :content-type content-type-status - :timestamp (or timestamp (time/now-ms))}))) - -(defn messages-with-timemarks [all-messages extras] - (let [status-message (get-intro-status-message all-messages) - all-messages (if status-message - (concat all-messages [status-message]) - all-messages) - messages (->> all-messages - (map #(merge % (get extras (:message-id %)))) - (remove #(false? (:show? %))) - (sort-by :clock-value >) - (map #(assoc % :datemark (time/day-relative (:timestamp %)))) - (group-by :datemark) - (vals) - (sort-by (comp :clock-value first) >) - (map (fn [v] [v {:type :datemark :value (:datemark (first v))}])) - (flatten)) - remove-last? (some (fn [{:keys [content-type]}] - (= content-type content-type-status)) - messages)] - (if remove-last? - (drop-last messages) - messages))) + [{:keys [group-chat current-public-key row]}] + (react/list-item [message/chat-message (assoc row + :group-chat group-chat + :current-public-key current-public-key)])) (defview messages-view [group-chat] - [messages [:chat :messages] - contacts [:chat :contacts] - message-extras [:get :message-extras] - loaded? [:all-messages-loaded?] - current-chat-id [:get-current-chat-id] - last-outgoing-message [:get-chat-last-outgoing-message @current-chat-id]] - (let [contacts' (contacts-by-identity contacts) - messages (messages-with-timemarks messages message-extras)] - [list-view {:renderRow (fn [row _ index] - (message-row {:contact-by-identity contacts' - :group-chat group-chat - :messages-count (count messages) - :row row - :index index - :last-outgoing? (= (:message-id last-outgoing-message) (:message-id row))})) - :renderScrollComponent #(invertible-scroll-view (js->clj %)) - :onEndReached (when-not loaded? #(dispatch [:load-more-messages])) - :enableEmptySections true - :keyboardShouldPersistTaps (if platform/android? :always :handled) - :dataSource (to-datasource-inverted messages)}])) + (letsubs [messages [:get-current-chat-messages] + current-public-key [:get-current-public-key]] + [react/list-view {:renderRow (fn [row _ index] + (message-row {:group-chat group-chat + :current-public-key current-public-key + :row row})) + :renderScrollComponent #(scroll-view/invertible-scroll-view (js->clj %)) + :onEndReached #(re-frame/dispatch [:load-more-messages]) + :enableEmptySections true + :keyboardShouldPersistTaps (if platform/android? :always :handled) + :dataSource (listview/to-datasource-inverted messages)}])) (defview chat [] - [group-chat [:chat :group-chat] - show-actions? [:get-current-chat-ui-prop :show-actions?] - show-bottom-info? [:get-current-chat-ui-prop :show-bottom-info?] - show-emoji? [:get-current-chat-ui-prop :show-emoji?] - layout-height [:get :layout-height] - input-text [:chat :input-text]] - {:component-did-mount #(dispatch [:check-and-open-dapp!]) - :component-will-unmount #(dispatch [:set-chat-ui-props {:show-emoji? false}])} - [view {:style st/chat-view - :on-layout (fn [event] - (let [height (.. event -nativeEvent -layout -height)] - (when (not= height layout-height) - (dispatch [:set-layout-height height]))))} - [chat-toolbar] - [messages-view group-chat] - [input/container {:text-empty? (str/blank? input-text)}] - (when show-actions? - [actions-view]) - (when show-bottom-info? - [bottom-info-view]) - [offline-view {:top (get-in platform-specific - [:component-styles :status-bar :default :height])}]]) + (letsubs [{:keys [group-chat input-text]} [:get-current-chat] + show-actions? [:get-current-chat-ui-prop :show-actions?] + show-bottom-info? [:get-current-chat-ui-prop :show-bottom-info?] + show-emoji? [:get-current-chat-ui-prop :show-emoji?] + layout-height [:get :layout-height]] + {:component-did-mount #(re-frame/dispatch [:check-and-open-dapp!]) + :component-will-unmount #(re-frame/dispatch [:set-chat-ui-props {:show-emoji? false}])} + [react/view {:style style/chat-view + :on-layout (fn [event] + (let [height (.. event -nativeEvent -layout -height)] + (when (not= height layout-height) + (re-frame/dispatch [:set-layout-height height]))))} + [chat-toolbar] + [messages-view group-chat] + [input/container {:text-empty? (string/blank? input-text)}] + (when show-actions? + [actions/actions-view]) + (when show-bottom-info? + [bottom-info/bottom-info-view]) + [offline/offline-view {:top (get-in platform/platform-specific + [:component-styles :status-bar :default :height])}]])) diff --git a/src/status_im/chat/sign_up.cljs b/src/status_im/chat/sign_up.cljs index 3caea29728..efb7bd6157 100644 --- a/src/status_im/chat/sign_up.cljs +++ b/src/status_im/chat/sign_up.cljs @@ -120,27 +120,16 @@ :from const/console-chat-id :to "me"}]]) -(def intro-status - {:message-id chat-const/intro-status-message-id - :content (label :t/intro-status) - :from const/console-chat-id - :chat-id const/console-chat-id - :content-type const/content-type-status - :outgoing false - :to "me"}) - -(def intro-events - [[:received-message intro-status] - [:received-message-when-commands-loaded - const/console-chat-id - {:chat-id const/console-chat-id - :message-id chat-const/intro-message1-id - :content {:command "password" - :content (label :t/intro-message1)} - :content-type const/content-type-command-request - :outgoing false - :from const/console-chat-id - :to "me"}]]) +(def intro-event + [:received-message-when-commands-loaded + {:chat-id const/console-chat-id + :message-id chat-const/intro-message1-id + :content {:command "password" + :content (label :t/intro-message1)} + :content-type const/content-type-command-request + :outgoing false + :from const/console-chat-id + :to "me"}]) (def console-chat {:chat-id const/console-chat-id @@ -161,4 +150,5 @@ :photo-path const/console-chat-id :dapp? true :unremovable? true - :bot-url "local://console-bot"}) + :bot-url "local://console-bot" + :status (label :t/intro-status)}) diff --git a/src/status_im/chat/specs.cljs b/src/status_im/chat/specs.cljs index cb3fda5714..8f9d33e2fa 100644 --- a/src/status_im/chat/specs.cljs +++ b/src/status_im/chat/specs.cljs @@ -1,27 +1,22 @@ (ns status-im.chat.specs (:require [cljs.spec.alpha :as s])) -(s/def :chat/chats (s/nilable map?)) ;; {id (string) chat (map)} active chats on chat's tab -(s/def :chat/current-chat-id (s/nilable string?)) ;;current or last opened chat-id -(s/def :chat/chat-id (s/nilable string?)) ;;what is the difference ? ^ -(s/def :chat/new-chat-name (s/nilable string?)) ;;we have name in the new-chat why do we need this field -(s/def :chat/chat-animations (s/nilable map?)) ;;{id (string) props (map)} -(s/def :chat/chat-ui-props (s/nilable map?)) ;;{id (string) props (map)} +(s/def :chat/chats (s/nilable map?)) ; {id (string) chat (map)} active chats on chat's tab +(s/def :chat/current-chat-id (s/nilable string?)) ; current or last opened chat-id +(s/def :chat/chat-id (s/nilable string?)) ; what is the difference ? ^ +(s/def :chat/new-chat-name (s/nilable string?)) ; we have name in the new-chat why do we need this field +(s/def :chat/chat-animations (s/nilable map?)) ; {id (string) props (map)} +(s/def :chat/chat-ui-props (s/nilable map?)) ; {id (string) props (map)} (s/def :chat/chat-list-ui-props (s/nilable map?)) -(s/def :chat/layout-height (s/nilable number?)) ;;height of chat's view layout -(s/def :chat/expandable-view-height-to-value (s/nilable number?)) -(s/def :chat/loading-allowed (s/nilable boolean?)) ;;allow to load more messages -(s/def :chat/message-data (s/nilable map?)) -(s/def :chat/message-id->transaction-id (s/nilable map?)) -(s/def :chat/message-status (s/nilable map?)) -(s/def :chat/unviewed-messages (s/nilable map?)) +(s/def :chat/layout-height (s/nilable number?)) ; height of chat's view layout +(s/def :chat/expandable-view-height-to-value (s/nilable number?)) +(s/def :chat/message-status (s/nilable map?)) ; TODO janherich: remove later (s/def :chat/selected-participants (s/nilable set?)) -(s/def :chat/chat-loaded-callbacks (s/nilable map?)) +(s/def :chat/chat-loaded-callbacks (s/nilable map?)) (s/def :chat/command-hash-valid? (s/nilable boolean?)) (s/def :chat/public-group-topic (s/nilable string?)) -(s/def :chat/confirmation-code-sms-listener (s/nilable any?)) ; .addListener result object -(s/def :chat/messages (s/nilable seq?)) -(s/def :chat/loaded-chats (s/nilable seq?)) -(s/def :chat/raw-unviewed-messages (s/nilable vector?)) +(s/def :chat/confirmation-code-sms-listener (s/nilable any?)) ; .addListener result object +(s/def :chat/messages (s/nilable map?)) ; messages indexed by message-id +(s/def :chat/loaded-chats (s/nilable seq?)) (s/def :chat/bot-db (s/nilable map?)) (s/def :chat/geolocation (s/nilable map?)) diff --git a/src/status_im/chat/styles/message/message.cljs b/src/status_im/chat/styles/message/message.cljs index fa69f467fe..a8f10156d2 100644 --- a/src/status_im/chat/styles/message/message.cljs +++ b/src/status_im/chat/styles/message/message.cljs @@ -1,46 +1,37 @@ (ns status-im.chat.styles.message.message (:require-macros [status-im.utils.styles :refer [defstyle defnstyle]]) - (:require [status-im.ui.components.styles :refer [color-white - color-black - color-blue - color-light-blue - selected-message-color - text1-color - text2-color - color-gray - color-gray4]] - [status-im.constants :refer [text-content-type - content-type-command]])) + (:require [status-im.ui.components.styles :as styles] + [status-im.constants :as constants])) (defstyle style-message-text {:fontSize 15 - :color text1-color + :color styles/text1-color :android {:line-height 22} :ios {:line-height 23}}) (def style-sub-text {:top -2 :fontSize 12 - :color text2-color + :color styles/text2-color :lineHeight 14 :height 16}) (defn message-padding-top - [{:keys [first-in-date? same-author same-direction]}] + [{:keys [first-in-date? same-author? same-direction?]}] (cond - first-in-date? 20 - same-author 8 - same-direction 16 - :else 24)) + first-in-date? 20 + same-author? 8 + same-direction? 16 + :else 24)) (defn last-message-padding - [{:keys [last-message typing]}] - (when (and last-message (not typing)) + [{:keys [last? typing]}] + (when (and last? (not typing)) {:paddingBottom 16})) (def message-datemark - {:margin-top 10 - :height 34}) + {:margin-top 10 + :height 34}) (def message-empty-spacing {:height 16}) @@ -65,7 +56,7 @@ {:marginTop 18 :marginLeft 40 :fontSize 12 - :color text2-color}) + :color styles/text2-color}) (def group-message-wrapper {:flexDirection :column}) @@ -94,7 +85,7 @@ :opacity 0.5}) (defstyle delivery-text - {:color color-gray4 + {:color styles/color-gray4 :marginLeft 5 :android {:font-size 13} :ios {:font-size 14}}) @@ -107,15 +98,15 @@ (defnstyle message-view [{:keys [content-type outgoing group-chat selected]}] (merge {:padding 12 - :backgroundColor color-white + :backgroundColor styles/color-white :android {:border-radius 4} :ios {:border-radius 8}} - (when (= content-type content-type-command) + (when (= content-type constants/content-type-command) {:paddingTop 10 :paddingBottom 14}))) (defstyle author - {:color color-gray4 + {:color styles/color-gray4 :margin-bottom 5 :android {:font-size 13} :ios {:font-size 14}}) @@ -127,7 +118,7 @@ {:borderRadius 14 :padding-vertical 10 :paddingRight 28 - :backgroundColor color-white}) + :backgroundColor styles/color-white}) (def command-request-from-text (merge style-sub-text {:marginBottom 2})) @@ -245,14 +236,14 @@ (def status-from {:marginTop 20 :fontSize 18 - :color text1-color}) + :color styles/text1-color}) (def status-text {:marginTop 10 :fontSize 14 :lineHeight 20 :textAlign :center - :color text2-color}) + :color styles/text2-color}) (defn message-animated-container [height] {:height height}) @@ -262,6 +253,6 @@ :width window-width}) (defn new-message-container [margin on-top?] - {:background-color color-white + {:background-color styles/color-white :margin-bottom margin :elevation (if on-top? 6 5)}) diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index 360524c982..631a37e479 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -1,18 +1,15 @@ (ns status-im.chat.subs - (:require [re-frame.core :refer [reg-sub dispatch subscribe path]] - [status-im.data-store.chats :as chats] - [status-im.chat.constants :as const] + (:require [re-frame.core :refer [reg-sub subscribe]] + [status-im.constants :as constants] [status-im.chat.models.input :as input-model] [status-im.chat.models.commands :as commands-model] [status-im.chat.utils :as chat-utils] [status-im.chat.views.input.utils :as input-utils] - [status-im.constants :refer [response-suggesstion-resize-duration - content-type-status - console-chat-id]] [status-im.commands.utils :as commands-utils] - [status-im.utils.platform :refer [platform-specific ios?]] - [taoensso.timbre :as log] - [clojure.string :as str])) + [status-im.utils.datetime :as time] + [status-im.utils.platform :as platform] + [status-im.i18n :as i18n] + [clojure.string :as string])) (reg-sub :chats :chats) @@ -49,14 +46,20 @@ :chat-input-margin :<- [:get :keyboard-height] (fn [kb-height] - (if ios? kb-height 0))) + (if platform/ios? kb-height 0))) + +(reg-sub + :get-chat + :<- [:chats] + (fn [chats [_ chat-id]] + (get chats chat-id))) (reg-sub :get-current-chat - :<- [:chats] - :<- [:get-current-chat-id] - (fn [[chats id]] - (get chats id))) + (fn [_] + (let [current-chat-id (subscribe [:get-current-chat-id])] + (subscribe [:get-chat @current-chat-id]))) + identity) (reg-sub :chat @@ -65,6 +68,78 @@ (fn [[chats id] [_ k chat-id]] (get-in chats [(or chat-id id) k]))) +(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." + [id->messages] + (let [datemark->messages (transduce (comp (map second) + (filter :show?) + (map (fn [{:keys [timestamp] :as msg}] + (assoc msg :datemark (time/day-relative timestamp))))) + (completing (fn [acc {:keys [datemark] :as msg}] + (update acc datemark conj msg))) + {} + id->messages)] + (->> datemark->messages + (map (fn [[datemark messages]] + [datemark (sort-by :clock-value > messages)])) + (sort-by (comp :clock-value first second) >)))) + +(reg-sub + :get-chat-message-datemark-groups + (fn [[_ chat-id]] + (subscribe [:get-chat chat-id])) + (fn [{:keys [messages]}] + (message-datemark-groups messages))) + +(defn messages-stream + "Transforms message-datemark-groups into flat sequence of messages interspersed with + datemark messages. + Additionaly enhances the messages in message sequence with derived stream context information, + like `:same-author?`, `:same-direction?`, `:last?` and `:last-outgoing?` flags + contact info/status + message for the last dategroup." + [[[last-datemark last-messages] :as message-datemark-groups]] + (if (seq message-datemark-groups) + (let [messages-seq (mapcat second message-datemark-groups) + {last-message-id :message-id} (first messages-seq) + {last-outgoing-message-id :message-id} (->> messages-seq + (filter :outgoing) + first)] + ;; TODO janherich: why the heck do we display contact user info/status in chat as a message in stream ? + ;; This makes no sense, user wants to have this information always available, not as something which + ;; scrolls with message stream + (->> (conj (rest message-datemark-groups) + [last-datemark (conj (into [] last-messages) {:content-type constants/content-type-status})]) + (mapcat (fn [[datemark messages]] + (let [prepared-messages (into [] + (map (fn [{:keys [message-id] :as message} previous-message] + (assoc message + :same-author? (= (:from message) + (:from previous-message)) + :same-direction? (= (:outgoing message) + (:outgoing previous-message)) + :last? (= message-id + last-message-id) + :last-outgoing? (= message-id + last-outgoing-message-id))) + messages + (concat (rest messages) '(nil))))] + (conj prepared-messages {:type :datemark + :value datemark})))))) + ;; when no messages are in chat, we need to at least fake-out today datemark + status messages + (list {:content-type constants/content-type-status} + {:type :datemark + :value (i18n/label :t/datetime-today)}))) + +(reg-sub + :get-current-chat-messages + (fn [_] + (let [current-chat-id (subscribe [:get-current-chat-id])] + (subscribe [:get-chat-message-datemark-groups @current-chat-id]))) + (fn [message-datemark-groups] + (messages-stream message-datemark-groups))) + (reg-sub :get-commands-for-chat :<- [:get-commands-responses-by-access-scope] @@ -80,27 +155,26 @@ :<- [:get-current-account] :<- [:get-current-chat] :<- [:get-contacts] - :<- [:chat :requests] - (fn [[commands-responses account chat contacts requests]] + (fn [[commands-responses account {:keys [requests] :as chat} contacts]] (commands-model/requested-responses commands-responses account chat contacts (vals requests)))) (def ^:private map->sorted-seq (comp (partial map second) (partial sort-by first))) -(defn- available-commands-responses [[commands-responses input-text]] +(defn- available-commands-responses [[commands-responses {:keys [input-text]}]] (->> commands-responses map->sorted-seq - (filter #(str/includes? (chat-utils/command-name %) (or input-text ""))))) + (filter #(string/includes? (chat-utils/command-name %) (or input-text ""))))) (reg-sub :get-available-commands :<- [:get-commands-for-chat] - :<- [:chat :input-text] + :<- [:get-current-chat] available-commands-responses) (reg-sub :get-available-responses :<- [:get-responses-for-chat] - :<- [:chat :input-text] + :<- [:get-current-chat] available-commands-responses) (reg-sub @@ -121,10 +195,9 @@ (reg-sub :current-chat-argument-position :<- [:selected-chat-command] - :<- [:chat :input-text] - :<- [:chat :seq-arguments] + :<- [:get-current-chat] :<- [:get-current-chat-ui-prop :selection] - (fn [[command input-text seq-arguments selection]] + (fn [[command {:keys [input-text seq-arguments]} selection]] (input-model/current-chat-argument-position command input-text selection seq-arguments))) (reg-sub @@ -150,9 +223,9 @@ :show-parameter-box? :<- [:chat-parameter-box] :<- [:show-suggestions?] - :<- [:chat :input-text] + :<- [:get-current-chat] :<- [:validation-messages] - (fn [[chat-parameter-box show-suggestions? input-text validation-messages]] + (fn [[chat-parameter-box show-suggestions? {:keys [input-text]} validation-messages]] (and (get chat-parameter-box :markup) (not validation-messages) (not show-suggestions?)))) @@ -165,24 +238,26 @@ (reg-sub :show-suggestions? :<- [:get-current-chat-ui-prop :show-suggestions?] - :<- [:chat :input-text] + :<- [:get-current-chat] :<- [:selected-chat-command] :<- [:get-available-commands-responses] - (fn [[show-suggestions? input-text selected-command commands-responses]] - (and (or show-suggestions? (input-model/starts-as-command? (str/trim (or input-text "")))) + (fn [[show-suggestions? {:keys [input-text]} selected-command commands-responses]] + (and (or show-suggestions? (input-model/starts-as-command? (string/trim (or input-text "")))) (not (:command selected-command)) (seq commands-responses)))) (reg-sub :is-request-answered? - :<- [:chat :requests] - (fn [requests [_ message-id]] + :<- [:get-current-chat] + (fn [{:keys [requests]} [_ message-id]] (not= "open" (get-in requests [message-id :status])))) (reg-sub :unviewed-messages-count - (fn [db [_ chat-id]] - (get-in db [:unviewed-messages chat-id :count]))) + (fn [[_ chat-id]] + (subscribe [:get-chat chat-id])) + (fn [{:keys [unviewed-messages]}] + (count unviewed-messages))) (reg-sub :web-view-extra-js @@ -190,42 +265,17 @@ (fn [current-chat] (:web-view-extra-js current-chat))) -(reg-sub - :all-messages-loaded? - :<- [:get-current-chat] - (fn [current-chat] - (:all-loaded? current-chat))) - (reg-sub :photo-path :<- [:get-contacts] (fn [contacts [_ id]] (:photo-path (contacts id)))) -;; TODO janherich: this is just bad and horribly ineffecient (always sorting to get last msg + -;; stale `:last-message` in app-db) refactor messages data-model to properly index them ASAP (reg-sub :get-last-message - :<- [:chats] - (fn [chats [_ chat-id]] - (let [{:keys [last-message messages]} (get chats chat-id)] - (->> (conj messages last-message) - (sort-by :clock-value >) - (filter :show?) - first)))) - -(reg-sub - :get-message-short-preview-markup - (fn [db [_ message-id]] - (get-in db [:message-data :short-preview message-id :markup]))) - -(reg-sub - :get-last-message-short-preview - (fn [db [_ chat-id]] - (let [last-message (subscribe [:get-last-message chat-id]) - preview (subscribe [:get-message-short-preview-markup (:message-id @last-message)])] - (when-let [markup @preview] - (commands-utils/generate-hiccup markup))))) + (fn [[_ chat-id]] + (subscribe [:get-chat-message-datemark-groups chat-id])) + (comp first second first)) (reg-sub :get-default-container-area-height @@ -250,25 +300,3 @@ (fn [db [_ key type]] (let [chat-id (subscribe [:get-current-chat-id])] (get-in db [:chat-animations @chat-id key type])))) - -(reg-sub - :get-chat-last-outgoing-message - :<- [:chats] - (fn [chats [_ chat-id]] - (->> (:messages (get chats chat-id)) - (filter :outgoing) - (sort-by :clock-value >) - first))) - -(reg-sub - :get-message-preview-markup - (fn [db [_ message-id]] - (get-in db [:message-data :preview message-id :markup]))) - -(reg-sub - :get-message-preview - (fn [[_ message-id]] - [(subscribe [:get-message-preview-markup message-id])]) - (fn [[markup]] - (when markup - (commands-utils/generate-hiccup markup)))) diff --git a/src/status_im/chat/utils.cljs b/src/status_im/chat/utils.cljs index e8b83698fc..95fa743aa5 100644 --- a/src/status_im/chat/utils.cljs +++ b/src/status_im/chat/utils.cljs @@ -1,47 +1,13 @@ (ns status-im.chat.utils - (:require [clojure.string :as str] - [status-im.constants :as consts] - [status-im.chat.constants :as chat-const])) - -(defn console? [s] - (= consts/console-chat-id s)) - -(def not-console? - (complement console?)) - -(defn safe-trim [s] - (when (string? s) - (str/trim s))) + (:require [status-im.chat.constants :as chat.constants])) (defn add-message-to-db ([db add-to-chat-id chat-id message] (add-message-to-db db add-to-chat-id chat-id message true)) - ([db add-to-chat-id chat-id message new?] - (let [messages [:chats add-to-chat-id :messages]] - (update-in db messages conj (assoc message :chat-id chat-id - :new? (if (nil? new?) - true - new?)))))) + ([db add-to-chat-id chat-id {:keys [message-id] :as message} new?] + (let [prepared-message (assoc message + :chat-id chat-id + :new? (if (nil? new?) true new?))] + (update-in db [:chats add-to-chat-id :messages] assoc message-id prepared-message)))) -(defn- check-message [previous-message {:keys [from outgoing] :as message}] - (merge message - {:same-author (if previous-message - (= (:from previous-message) from) - true) - :same-direction (if previous-message - (= (:outgoing previous-message) outgoing) - true)})) - -(defn check-author-direction - ([previous-message message] - (check-message previous-message message)) - ([db chat-id message] - (let [previous-message (first (get-in db [:chats chat-id :messages]))] - (check-message previous-message message)))) - -(defn command-name [{:keys [bot name scope]}] - (cond - (:global? scope) - (str chat-const/bot-char name) - - :default - (str chat-const/command-char name))) +(defn command-name [{:keys [name]}] + (str chat.constants/command-char name)) diff --git a/src/status_im/chat/views/input/input.cljs b/src/status_im/chat/views/input/input.cljs index edf4632840..98324640b0 100644 --- a/src/status_im/chat/views/input/input.cljs +++ b/src/status_im/chat/views/input/input.cljs @@ -121,7 +121,7 @@ (let [input (str/trim (or @input-text "")) real-args (remove str/blank? (:args command))] (when-let [placeholder (cond - (#{const/command-char const/bot-char} input) + (= const/command-char input) (i18n/label :t/type-a-command) (and command (empty? real-args)) diff --git a/src/status_im/chat/views/message/datemark.cljs b/src/status_im/chat/views/message/datemark.cljs index 8ed52f20a2..0002f44a5f 100644 --- a/src/status_im/chat/views/message/datemark.cljs +++ b/src/status_im/chat/views/message/datemark.cljs @@ -1,13 +1,10 @@ (ns status-im.chat.views.message.datemark - (:require [re-frame.core :refer [subscribe dispatch]] - [status-im.ui.components.react :refer [view - text]] - [clojure.string :as str] - [status-im.i18n :refer [label]] + (:require [status-im.ui.components.react :as react] + [clojure.string :as str] [status-im.chat.styles.message.datemark :as st])) (defn chat-datemark [value] - [view st/datemark-wrapper - [view st/datemark - [text {:style st/datemark-text} - (str/capitalize (or value (label :t/datetime-today)))]]]) \ No newline at end of file + [react/view st/datemark-wrapper + [react/view st/datemark + [react/text {:style st/datemark-text} + (str/capitalize value)]]]) diff --git a/src/status_im/chat/views/message/message.cljs b/src/status_im/chat/views/message/message.cljs index e6740e598d..9d0de1d8f1 100644 --- a/src/status_im/chat/views/message/message.cljs +++ b/src/status_im/chat/views/message/message.cljs @@ -1,154 +1,97 @@ (ns status-im.chat.views.message.message (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :refer [subscribe dispatch]] + (:require [re-frame.core :as re-frame] [clojure.walk :as walk] - [reagent.core :as r] - [status-im.i18n :refer [message-status-label]] - [status-im.ui.components.react :refer [view - text - image - icon - animated-view - touchable-without-feedback - touchable-highlight - autolink - get-dimensions - dismiss-keyboard!]] - [status-im.ui.components.animation :as anim] - [status-im.ui.components.list-selection :refer [share share-or-open-map]] - [status-im.chat.constants :as chat-consts] + [reagent.core :as reagent] + [status-im.ui.components.react :as react] + [status-im.ui.components.animation :as animation] + [status-im.ui.components.list-selection :as list-selection] [status-im.chat.models.commands :as commands] - [status-im.chat.styles.message.message :as st] - [status-im.chat.styles.message.command-pill :as pill-st] - [status-im.chat.views.message.request-message :refer [message-content-command-request]] - [status-im.chat.views.message.datemark :refer [chat-datemark]] - [status-im.react-native.resources :as res] - [status-im.constants :refer [console-chat-id - text-content-type - content-type-log-message - content-type-status - content-type-command - content-type-command-request] :as c] - [status-im.ui.components.chat-icon.screen :refer [chat-icon-message-status]] - [status-im.utils.identicon :refer [identicon]] - [status-im.utils.gfycat.core :refer [generate-gfy]] + [status-im.commands.utils :as commands.utils] + [status-im.chat.utils :as chat.utils] + [status-im.chat.styles.message.message :as style] + [status-im.chat.styles.message.command-pill :as pill-style] + [status-im.chat.views.message.request-message :as request-message] + [status-im.constants :as constants] + [status-im.ui.components.chat-icon.screen :as chat-icon.screen] + [status-im.utils.identicon :as identicon] + [status-im.utils.gfycat.core :as gfycat] [status-im.utils.platform :as platform] - [status-im.i18n :refer [label - get-contact-translated]] - [status-im.chat.utils :as cu] - [clojure.string :as str] - [status-im.chat.events.console :as console] - [taoensso.timbre :as log])) + [status-im.i18n :as i18n] + [clojure.string :as string] + [status-im.chat.events.console :as console])) -(def window-width (:width (get-dimensions "window"))) +(def window-width (:width (react/get-dimensions "window"))) (defview message-author-name [{:keys [outgoing from] :as message}] - [current-account [:get-current-account] - incoming-name [:contact-name-by-identity from]] - (if-let [name (if outgoing - (:name current-account) - (or incoming-name "Unknown contact"))] - [text {:style st/author} name])) + (letsubs [current-account [:get-current-account] + incoming-name [:contact-name-by-identity from]] + (when-let [name (if outgoing + (:name current-account) + (or incoming-name "Unknown contact"))] + [react/text {:style style/author} name]))) -(defview message-content-status - [{:keys [messages-count content datemark]}] - (letsubs [chat-id [:chat :chat-id] - group-chat [:chat :group-id] - name [:chat :name] - color [:chat :color] - public-key [:chat :public-key] - members [:current-chat-contacts]] - (let [{:keys [status]} (if group-chat - {:photo-path nil - :status nil - :last-online 0} +(defview message-content-status [] + (letsubs [{:keys [chat-id group-id name color public-key]} [:get-current-chat] + members [:current-chat-contacts]] + (let [{:keys [status]} (if group-id + {:status nil} (first members))] - [view st/status-container - [chat-icon-message-status chat-id group-chat name color false] - [text {:style st/status-from - :font :default - :number-of-lines 1} - (if (str/blank? name) - (generate-gfy public-key) - (or (get-contact-translated chat-id :name name) - (label :t/chat-name)))] - (when (or status content) - [text {:style st/status-text - :font :default} - (or status content)]) - (if (> messages-count 1) - [view st/message-datemark - [chat-datemark datemark]] - [view st/message-empty-spacing])]))) + [react/view style/status-container + [chat-icon.screen/chat-icon-message-status chat-id group-id name color false] + [react/text {:style style/status-from + :font :default + :number-of-lines 1} + (if (string/blank? name) + (gfycat/generate-gfy public-key) + (or (i18n/get-contact-translated chat-id :name name) + (i18n/label :t/chat-name)))] + (when status + [react/text {:style style/status-text + :font :default} + status])]))) (defn message-content-audio [_] - [view st/audio-container - [view st/play-view - [image {;:source res/play - :style st/play-image}]] - [view st/track-container - [view st/track] - [view st/track-mark] - [text {:style st/track-duration-text - :font :default} + [react/view style/audio-container + [react/view style/play-view + [react/image {:style style/play-image}]] + [react/view style/track-container + [react/view style/track] + [react/view style/track-mark] + [react/text {:style style/track-duration-text + :font :default} "03:39"]]]) -(defn wallet-command-preview - [{{:keys [name]} :contact-chat - :keys [contact-address params outgoing? current-chat-id]}] - (let [{:keys [recipient amount]} (walk/keywordize-keys params)] - [text {:style st/command-text - :font :default} - (label :t/chat-send-eth {:amount amount})])) - -(defn wallet-command? [content-type] - (#{c/content-type-wallet-command c/content-type-wallet-request} content-type)) - -(defn command-preview - [{:keys [params preview content-type] :as message}] - (cond - (wallet-command? content-type) - (wallet-command-preview message) - - preview preview - - :else - [text {:style st/command-text - :font :default} - (if (= 1 (count params)) - (first (vals params)) - (str params))])) - (defview message-content-command - [{:keys [message-id content content-type chat-id to from outgoing] :as message}] - (letsubs [command [:get-command (:content-command-ref content)] - current-chat-id [:get-current-chat-id] - contact-chat [:get-in [:chats (if outgoing to from)]] - preview [:get-message-preview message-id]] - (let [{:keys [name type] - icon-path :icon} command] - [view st/content-command-view - (when (:color command) - [view st/command-container - [view (pill-st/pill command) - [text {:style pill-st/pill-text - :font :default} - (str chat-consts/command-char name)]]]) + [{:keys [content params] :as message}] + (letsubs [command [:get-command (:content-command-ref content)]] + {:component-will-mount #(when-not (:preview content) + (re-frame/dispatch [:request-command-message-data + message {:data-type :preview + :cache-data? true}]))} + (let [preview (:preview content) + {:keys [type color] icon-path :icon} command] + [react/view style/content-command-view + (when color + [react/view style/command-container + [react/view (pill-style/pill command) + [react/text {:style pill-style/pill-text + :font :default} + (chat.utils/command-name command)]]]) (when icon-path - [view st/command-image-view - [icon icon-path st/command-image]]) - [command-preview {:command (:name command) - :content-type content-type - :params (:params content) - :outgoing? outgoing - :preview preview - :contact-chat contact-chat - :contact-address (if outgoing to from) - :current-chat-id current-chat-id}]]))) + [react/view style/command-image-view + [react/icon icon-path style/command-image]]) + (if (:markup preview) + ;; Markup was defined for command in jail, generate hiccup and render it + (commands.utils/generate-hiccup (:markup preview)) + ;; Display preview if it's defined (as a string), in worst case, render params + [react/text {:style style/command-text + :font :default} + (or preview (str params))])]))) (defn message-view - [{:keys [same-author index group-chat] :as message} content] - [view (st/message-view message) + [{:keys [group-chat] :as message} content] + [react/view (style/message-view message) (when group-chat [message-author-name message]) content]) @@ -156,33 +99,31 @@ {"\\*[^*]+\\*" {:font-weight :bold} "~[^~]+~" {:font-style :italic}}) -(def regx (re-pattern (str/join "|" (map first replacements)))) +(def regx (re-pattern (string/join "|" (map first replacements)))) (defn get-style [string] (->> replacements - (into [] (comp - (map first) - (map #(vector % (re-pattern %))) - (drop-while (fn [[_ regx]] (not (re-matches regx string)))) - (take 1))) + (into [] (comp (map first) + (map #(vector % (re-pattern %))) + (drop-while (fn [[_ regx]] (not (re-matches regx string)))) + (take 1))) ffirst replacements)) ;; todo rewrite this, naive implementation (defn- parse-text [string] (if (string? string) - (let [general-text (str/split string regx) + (let [general-text (string/split string regx) general-text' (if (zero? (count general-text)) [nil] general-text) - styled-text (vec (map-indexed - (fn [idx string] - (let [style (get-style string)] - [text - {:key (str idx "_" string) - :style style} - (subs string 1 (dec (count string)))])) - (re-seq regx string))) + styled-text (vec (map-indexed (fn [idx string] + (let [style (get-style string)] + [react/text + {:key (str idx "_" string) + :style style} + (subs string 1 (dec (count string)))])) + (re-seq regx string))) styled-text' (if (> (count general-text) (count styled-text)) (conj styled-text nil) @@ -197,41 +138,31 @@ simple-text? (and (= (count parsed-text) 2) (nil? (second parsed-text)))] (if simple-text? - [autolink {:style (st/text-message message) - :text (apply str parsed-text) - :onPress #(dispatch [:browse-link-from-message %])}] - [text {:style (st/text-message message)} parsed-text]))]) + [react/autolink {:style (style/text-message message) + :text (apply str parsed-text) + :onPress #(re-frame/dispatch [:browse-link-from-message %])}] + [react/text {:style (style/text-message message)} parsed-text]))]) (defmulti message-content (fn [_ message _] (message :content-type))) -(defmethod message-content content-type-command-request +(defmethod message-content constants/content-type-command-request [wrapper message] [wrapper message - [message-view message [message-content-command-request message]]]) + [message-view message [request-message/message-content-command-request message]]]) -(defmethod message-content c/content-type-wallet-request - [wrapper message] - [wrapper message - [message-view message [message-content-command-request message]]]) - -(defmethod message-content text-content-type +(defmethod message-content constants/text-content-type [wrapper message] [wrapper message [text-message message]]) -(defmethod message-content content-type-log-message +(defmethod message-content constants/content-type-log-message [wrapper message] [wrapper message [text-message message]]) -(defmethod message-content content-type-status +(defmethod message-content constants/content-type-status [_ message] - [message-content-status message]) + [message-content-status]) -(defmethod message-content content-type-command - [wrapper message] - [wrapper message - [message-view message [message-content-command message]]]) - -(defmethod message-content c/content-type-wallet-command +(defmethod message-content constants/content-type-command [wrapper message] [wrapper message [message-view message [message-content-command message]]]) @@ -244,162 +175,150 @@ :content-type content-type}]]]) (defview group-message-delivery-status [{:keys [message-id group-id message-status user-statuses] :as msg}] - [app-db-message-user-statuses [:get-in [:message-data :user-statuses message-id]] - app-db-message-status-value [:get-in [:message-data :statuses message-id :status]] - chat [:get-current-chat] - contacts [:get-contacts]] - (let [status (or message-status app-db-message-status-value :sending) - user-statuses (merge user-statuses app-db-message-user-statuses) - participants (:contacts chat) - seen-by-everyone? (and (= (count user-statuses) (count participants)) - (every? (fn [[_ {:keys [status]}]] - (= (keyword status) :seen)) user-statuses))] - (if (or (zero? (count user-statuses)) - seen-by-everyone?) - [view st/delivery-view - [text {:style st/delivery-text - :font :default} - (message-status-label - (if seen-by-everyone? - :seen-by-everyone - status))]] - [touchable-highlight - {:on-press (fn [] - (dispatch [:show-message-details {:message-status status - :user-statuses user-statuses - :participants participants}]))} - [view st/delivery-view - (for [[_ {:keys [whisper-identity]}] (take 3 user-statuses)] - ^{:key whisper-identity} - [image {:source {:uri (or (get-in contacts [whisper-identity :photo-path]) - (identicon whisper-identity))} - :style {:width 16 - :height 16 - :borderRadius 8}}]) - (if (> (count user-statuses) 3) - [text {:style st/delivery-text - :font :default} - (str "+ " (- (count user-statuses) 3))])]]))) + (letsubs [chat [:get-current-chat] + contacts [:get-contacts]] + (let [status (or message-status :sending) + participants (:contacts chat) + seen-by-everyone? (and (= (count user-statuses) (count participants)) + (every? (fn [[_ {:keys [status]}]] + (= (keyword status) :seen)) user-statuses))] + (if (or (zero? (count user-statuses)) + seen-by-everyone?) + [react/view style/delivery-view + [react/text {:style style/delivery-text + :font :default} + (i18n/message-status-label + (if seen-by-everyone? + :seen-by-everyone + status))]] + [react/touchable-highlight + {:on-press (fn [] + (re-frame/dispatch [:show-message-details {:message-status status + :user-statuses user-statuses + :participants participants}]))} + [react/view style/delivery-view + (for [[_ {:keys [whisper-identity]}] (take 3 user-statuses)] + ^{:key whisper-identity} + [react/image {:source {:uri (or (get-in contacts [whisper-identity :photo-path]) + (identicon/identicon whisper-identity))} + :style {:width 16 + :height 16 + :borderRadius 8}}]) + (if (> (count user-statuses) 3) + [react/text {:style style/delivery-text + :font :default} + (str "+ " (- (count user-statuses) 3))])]])))) -(defview message-delivery-status +(defn message-delivery-status [{:keys [message-id chat-id message-status user-statuses content]}] - [app-db-message-status-value [:get-in [:message-data :statuses message-id :status]]] (let [delivery-status (get-in user-statuses [chat-id :status]) status (cond (and (not (console/commands-with-delivery-status (:command content))) - (cu/console? chat-id)) + (= constants/console-chat-id chat-id)) :seen :else - (or delivery-status message-status app-db-message-status-value :sending))] - [view st/delivery-view - [text {:style st/delivery-text - :font :default} - (message-status-label status)]])) + (or delivery-status message-status :sending))] + [react/view style/delivery-view + [react/text {:style style/delivery-text + :font :default} + (i18n/message-status-label status)]])) (defview member-photo [from] - [photo-path [:photo-path from]] - [view - [image {:source {:uri (if (str/blank? photo-path) - (identicon from) - photo-path)} - :style st/photo}]]) + (letsubs [photo-path [:photo-path from]] + [react/view + [react/image {:source {:uri (if (string/blank? photo-path) + (identicon/identicon from) + photo-path)} + :style style/photo}]])) (defview my-photo [from] - [account [:get-current-account]] - (let [{:keys [photo-path]} account] - [view - [image {:source {:uri (if (str/blank? photo-path) - (identicon from) - photo-path)} - :style st/photo}]])) + (letsubs [account [:get-current-account]] + (let [{:keys [photo-path]} account] + [react/view + [react/image {:source {:uri (if (string/blank? photo-path) + (identicon/identicon from) + photo-path)} + :style style/photo}]]))) (defn message-body - [{:keys [last-outgoing? message-type same-author from index outgoing] :as message} content] - (let [delivery-status :seen-by-everyone] - [view st/group-message-wrapper - [view (st/message-body message) - [view st/message-author - (when (or (= index 1) (not same-author)) - (if outgoing - [my-photo from] - [member-photo from]))] - [view (st/group-message-view message) - content - (when last-outgoing? - (if (= (keyword message-type) :group-user-message) - [group-message-delivery-status message] - [message-delivery-status message]))]]])) + [{:keys [last-outgoing? message-type same-author? from outgoing] :as message} content] + [react/view style/group-message-wrapper + [react/view (style/message-body message) + [react/view style/message-author + (when-not same-author? + (if outgoing + [my-photo from] + [member-photo from]))] + [react/view (style/group-message-view message) + content + (when last-outgoing? + (if (= (keyword message-type) :group-user-message) + [group-message-delivery-status message] + [message-delivery-status message]))]]]) (defn message-container-animation-logic [{:keys [to-value val callback]}] (fn [_] (let [to-value @to-value] (when (pos? to-value) - (anim/start - (anim/timing val {:toValue to-value - :duration 250}) + (animation/start + (animation/timing val {:toValue to-value + :duration 250}) (fn [arg] (when (.-finished arg) (callback)))))))) (defn message-container [message & children] (if (:new? message) - (let [layout-height (r/atom 0) - anim-value (anim/create-value 1) - anim-callback #(dispatch [:set-message-shown message]) + (let [layout-height (reagent/atom 0) + anim-value (animation/create-value 1) + anim-callback #(re-frame/dispatch [:set-message-shown message]) context {:to-value layout-height :val anim-value :callback anim-callback} on-update (message-container-animation-logic context)] - (r/create-class + (reagent/create-class {:component-did-update on-update :display-name "message-container" :reagent-render (fn [_ & children] @layout-height - [animated-view {:style (st/message-animated-container anim-value)} - (into [view {:style (st/message-container window-width) - :onLayout (fn [event] - (let [height (.. event -nativeEvent -layout -height)] - (reset! layout-height height)))}] + [react/animated-view {:style (style/message-animated-container anim-value)} + (into [react/view {:style (style/message-container window-width) + :onLayout (fn [event] + (let [height (.. event -nativeEvent -layout -height)] + (reset! layout-height height)))}] children)])})) - (into [view] children))) + (into [react/view] children))) -(defn chat-message [{:keys [outgoing message-id chat-id user-statuses from] :as message}] - (let [my-identity (subscribe [:get :current-public-key]) - status (subscribe [:get-in [:message-data :user-statuses message-id my-identity]]) - preview (subscribe [:get-message-preview message-id])] - (r/create-class - {:display-name "chat-message" - :component-will-mount - (fn [] - (let [{:keys [bot command] :as content} (get-in message [:content]) - message' (assoc message :jail-id bot)] - (when (and command (not @preview)) - (dispatch [:request-command-preview message'])))) - - :component-did-mount - (fn [] - (when (and (not outgoing) - (not= :seen (keyword @status)) - (not= :seen (keyword (get-in user-statuses [@my-identity :status])))) - (dispatch [:send-seen! {:chat-id chat-id - :from from - :message-id message-id}]))) - :reagent-render - (fn [{:keys [outgoing group-chat content-type content] :as message}] - [message-container message - [touchable-highlight {:on-press #(when platform/ios? - (dispatch [:set-chat-ui-props - {:show-emoji? false}]) - (dismiss-keyboard!)) - :on-long-press #(cond (= content-type text-content-type) - (share content (label :t/message)) - (and (= content-type content-type-command) (= "location" (:content-command content))) - (let [address (get-in content [:params :address]) - [location lat long] (str/split address #"&")] - (share-or-open-map location lat long)))} - [view - (let [incoming-group (and group-chat (not outgoing))] - [message-content message-body (merge message - {:incoming-group incoming-group})])]]])}))) +(defn chat-message [{:keys [outgoing message-id chat-id message-status user-statuses + from current-public-key] :as message}] + (reagent/create-class + {:display-name "chat-message" + :component-did-mount + #(when (and message-id + chat-id + (not outgoing) + (not= :seen message-status) + (not= :seen (keyword (get-in user-statuses [current-public-key :status])))) + (re-frame/dispatch [:send-seen! {:chat-id chat-id + :from from + :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? + (re-frame/dispatch [:set-chat-ui-props + {:show-emoji? false}]) + (react/dismiss-keyboard!)) + :on-long-press #(cond (= content-type constants/text-content-type) + (list-selection/share content (i18n/label :t/message)) + (and (= content-type constants/content-type-command) + (= "location" (:content-command content))) + (let [address (get-in content [:params :address]) + [location lat long] (string/split address #"&")] + (list-selection/share-or-open-map location lat long)))} + [react/view + (let [incoming-group (and group-chat (not outgoing))] + [message-content message-body (merge message + {: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 a42425e581..d91c0fe1c1 100644 --- a/src/status_im/chat/views/message/request_message.cljs +++ b/src/status_im/chat/views/message/request_message.cljs @@ -10,6 +10,7 @@ touchable-highlight]] [status-im.chat.styles.message.message :as st] [status-im.chat.models.commands :as commands] + [status-im.commands.utils :as commands-utils] [status-im.ui.components.animation :as anim] [taoensso.timbre :as log])) @@ -72,12 +73,15 @@ [icon command-icon st/command-request-image])]]))}))) (defview message-content-command-request - [{:keys [message-id chat-id content from incoming-group] :as message}] + [{:keys [message-id content] :as message}] (letsubs [command [:get-command (:content-command-ref content)] answered? [:is-request-answered? message-id] - status-initialized? [:get :status-module-initialized?] - markup [:get-message-preview message-id]] - (let [{:keys [prefill prefill-bot-db prefillBotDb params] + status-initialized? [:get :status-module-initialized?]] + {:component-will-mount #(when-not (:preview content) + (dispatch [:request-command-message-data + message {:data-type :preview + :cache-data? true}]))} + (let [{:keys [prefill prefill-bot-db prefillBotDb params preview] text-content :text} content command (if (and params command) (merge command {:prefill prefill @@ -91,12 +95,11 @@ [touchable-highlight {:on-press on-press-handler} [view st/command-request-message-view - (if (and markup - (not (string? markup))) - [view markup] + (if (:markup preview) + [view (commands-utils/generate-hiccup (:markup preview))] [text {:style st/style-message-text :font :default} - (or text-content markup (:content content))])]] + (or preview text-content (:content content))])]] (when (:request-text command) [view st/command-request-text-view [text {:style st/style-sub-text diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 961b749730..7566a67c0d 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -13,9 +13,7 @@ (def text-content-type "text/plain") (def content-type-log-message "log-message") (def content-type-command "command") -(def content-type-command-request "command-request") -(def content-type-wallet-command "wallet-command") -(def content-type-wallet-request "wallet-request") +(def content-type-command-request "command-request") (def content-type-status "status") (def min-password-length 6) diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index 12199203cb..3266528f51 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -19,10 +19,8 @@ (data-store/exists? chat-id)) (defn save - [{:keys [last-message-id chat-id] :as chat}] - ;; TODO(janherich): remove `:last-message-id`, seems like it's not used anywhere anymore - (let [chat (assoc chat :last-message-id (or last-message-id ""))] - (data-store/save chat (data-store/exists? chat-id)))) + [{:keys [last-message-id chat-id] :as chat}] + (data-store/save chat (data-store/exists? chat-id))) (defn delete [chat-id] diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 70848deb17..6f0c287c79 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -9,15 +9,12 @@ (defn- command-type? [type] (contains? - #{constants/content-type-command constants/content-type-command-request - constants/content-type-wallet-request constants/content-type-wallet-command} + #{constants/content-type-command constants/content-type-command-request} type)) (def default-values {:outgoing false :to nil - :same-author false - :same-direction false :preview nil}) (defn exists? [message-id] @@ -32,10 +29,6 @@ (when (command-type? content-type) (reader/read-string content)))) -(defn get-count-by-chat-id - [chat-id] - (data-store/get-count-by-chat-id chat-id)) - (defn get-by-chat-id ([chat-id] (get-by-chat-id chat-id 0)) @@ -53,13 +46,6 @@ (filter #(= (:content-type %) constants/content-type-log-message)) (map #(select-keys % [:content :timestamp])))) -(defn get-last-message - [chat-id] - (if-let [{:keys [content-type] :as message} (data-store/get-last-message chat-id)] - (if (command-type? content-type) - (update message :content reader/read-string) - message))) - (defn get-last-outgoing [chat-id number-of-messages] (data-store/get-by-fields {:chat-id chat-id @@ -77,25 +63,19 @@ [] (data-store/get-unviewed)) -(defn get-previews - [] - (->> (data-store/get-all-as-list) - (filter :preview) - (reduce (fn [acc {:keys [message-id preview]}] - (assoc acc message-id (reader/read-string preview))) - {}))) - (defn- prepare-content [content] - (pr-str - (update content :params dissoc :password :password-confirmation))) + (if (string? content) + content + (pr-str + ;; TODO janherich: this is ugly and not systematic, define something like `:not-persisent` + ;; option for command params instead + (update content :params dissoc :password :password-confirmation)))) (defn save ;; todo remove chat-id parameter [chat-id {:keys [message-id content] :as message}] (when-not (data-store/exists? message-id) - (let [content' (if (string? content) - content - (prepare-content content)) + (let [content' (prepare-content content) message' (merge default-values message {:chat-id chat-id @@ -106,7 +86,9 @@ (defn update-message [{:keys [message-id] :as message}] (when (data-store/exists? message-id) - (let [message (utils/update-if-present message :user-statuses vals)] + (let [message (-> message + (utils/update-if-present :user-statuses vals) + (utils/update-if-present :content prepare-content))] (data-store/save message)))) (defn delete-by-chat-id [chat-id] diff --git a/src/status_im/data_store/realm/messages.cljs b/src/status_im/data_store/realm/messages.cljs index cec80388e5..eeaf74d316 100644 --- a/src/status_im/data_store/realm/messages.cljs +++ b/src/status_im/data_store/realm/messages.cljs @@ -16,10 +16,7 @@ (when-let [message (realm/get-one-by-field-clj @realm/account-realm :message :message-id message-id)] (realm/fix-map message :user-statuses :whisper-identity))) -(defn get-by-chat-id - "arity-1 returns realm object for queries" - ([chat-id] - (realm/get-by-field @realm/account-realm :message :chat-id chat-id)) +(defn get-by-chat-id ([chat-id number-of-messages] (get-by-chat-id chat-id 0 number-of-messages)) ([chat-id from number-of-messages] @@ -29,10 +26,6 @@ realm/js-object->clj)] (mapv #(realm/fix-map % :user-statuses :whisper-identity) messages)))) -(defn get-count-by-chat-id - [chat-id] - (realm/get-count (get-by-chat-id chat-id))) - (defn get-by-fields [fields from number-of-messages] (-> (realm/get-by-fields @realm/account-realm :message :and fields) @@ -64,5 +57,6 @@ (defn delete-by-chat-id [chat-id] - (realm/delete @realm/account-realm - (get-by-chat-id chat-id))) + (let [current-realm @realm/account-realm] + (realm/delete current-realm + (realm/get-by-field current-realm :message :chat-id chat-id)))) diff --git a/src/status_im/data_store/realm/schemas/account/v19/chat.cljs b/src/status_im/data_store/realm/schemas/account/v19/chat.cljs new file mode 100644 index 0000000000..25aeafc4e0 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v19/chat.cljs @@ -0,0 +1,39 @@ +(ns status-im.data-store.realm.schemas.account.v19.chat + (:require [status-im.ui.components.styles :refer [default-chat-color]])) + +(def schema {:name :chat + :primaryKey :chat-id + :properties {:chat-id :string + :name :string + :color {:type :string + :default default-chat-color} + :group-chat {:type :bool + :indexed true} + :group-admin {:type :string + :optional true} + :is-active :bool + :timestamp :int + :contacts {:type :list + :objectType :chat-contact} + :unremovable? {:type :bool + :default false} + :removed-at {:type :int + :optional true} + :removed-from-at {:type :int + :optional true} + :added-to-at {:type :int + :optional true} + :updated-at {:type :int + :optional true} + :message-overhead {:type :int + :default 0} + :public-key {:type :string + :optional true} + :private-key {:type :string + :optional true} + :contact-info {:type :string + :optional true} + :debug? {:type :bool + :default false} + :public? {:type :bool + :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/account/v19/core.cljs b/src/status_im/data_store/realm/schemas/account/v19/core.cljs index 0ad8d580c7..d4fbecd48a 100644 --- a/src/status_im/data_store/realm/schemas/account/v19/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/v19/core.cljs @@ -1,9 +1,9 @@ (ns status-im.data-store.realm.schemas.account.v19.core - (:require [status-im.data-store.realm.schemas.account.v11.chat :as chat] - [status-im.data-store.realm.schemas.account.v1.chat-contact :as chat-contact] + (:require [status-im.data-store.realm.schemas.account.v19.chat :as chat] + [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.v1.discover :as discover] - [status-im.data-store.realm.schemas.account.v10.message :as message] + [status-im.data-store.realm.schemas.account.v1.discover :as discover] + [status-im.data-store.realm.schemas.account.v19.message :as message] [status-im.data-store.realm.schemas.account.v12.pending-message :as pending-message] [status-im.data-store.realm.schemas.account.v1.processed-message :as processed-message] [status-im.data-store.realm.schemas.account.v19.request :as request] @@ -11,14 +11,14 @@ [status-im.data-store.realm.schemas.account.v1.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.v8.local-storage :as local-storage] [taoensso.timbre :as log] [cljs.reader :as reader])) (def schema [chat/schema - chat-contact/schema + chat-contact/schema contact/schema - discover/schema + discover/schema message/schema pending-message/schema processed-message/schema @@ -29,12 +29,19 @@ group-contact/schema local-storage/schema]) +(defn remove-console-intro-message! [new-realm] + (when-let [console-intro-message (some-> new-realm + (.objects "message") + (.filtered (str "message-id = \"intro-status\"")) + (aget 0))] + (log/debug "v19 Removing console intro message " (pr-str console-intro-message)))) + (defn remove-contact! [new-realm whisper-identity] (when-let [contact (some-> new-realm (.objects "contact") (.filtered (str "whisper-identity = \"" whisper-identity "\"")) (aget 0))] - (log/debug "v19 Removing contact" (pr-str contact)) + (log/debug "v19 Removing contact " (pr-str contact)) (.delete new-realm contact))) (def owner-command->new-props @@ -75,9 +82,13 @@ (def transactor-requests->new-props {;; former transactor-personal request - ["send" 1] {:content-command-ref ["transactor" :response 83 "send"]} + ["send" 1] {:content-command-ref ["transactor" :response 83 "send"] + :content-command-scope-bitmask 83 + :bot "transactor"} ;; former transactor-group request - ["send" 2] {:content-command-ref ["transactor" :response 85 "send"]}}) + ["send" 2] {:content-command-ref ["transactor" :response 85 "send"] + :content-command-scope-bitmask 85 + :bot "transactor"}}) (defn update-commands [selector mapping new-realm content-type] (some-> new-realm @@ -94,6 +105,7 @@ (log/debug "migrating v19 account database: " old-realm new-realm) (remove-contact! new-realm "transactor-personal") (remove-contact! new-realm "transactor-group") + (remove-console-intro-message! new-realm) (update-commands (juxt :bot :command) owner-command->new-props new-realm "command") (update-commands (juxt :command) console-requests->new-props new-realm "command-request") (update-commands (juxt :command (comp count :prefill)) transactor-requests->new-props new-realm "command-request")) diff --git a/src/status_im/data_store/realm/schemas/account/v19/message.cljs b/src/status_im/data_store/realm/schemas/account/v19/message.cljs new file mode 100644 index 0000000000..346ff1a5c7 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/v19/message.cljs @@ -0,0 +1,30 @@ +(ns status-im.data-store.realm.schemas.account.v19.message) + +(def schema {:name :message + :primaryKey :message-id + :properties {:message-id :string + :from :string + :to {:type :string + :optional true} + :group-id {: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"} + :clock-value {:type :int + :default 0} + :show? {:type :bool + :default true}}}) diff --git a/src/status_im/native_module/impl/non_status_go_module.cljs b/src/status_im/native_module/impl/non_status_go_module.cljs index aef4ade648..858c7a1db1 100644 --- a/src/status_im/native_module/impl/non_status_go_module.cljs +++ b/src/status_im/native_module/impl/non_status_go_module.cljs @@ -33,7 +33,7 @@ (cond (= path [:responses "password" :preview]) (callback {:result {:context {}, - :messages [], + :messages {}, :returned {:markup ["text" {:style {:color "black", diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index a4839275e7..520559fa44 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -309,6 +309,11 @@ ;;; MESSAGES +(defn- transform-protocol-message [{:keys [from to payload]}] + (merge payload {:from from + :to to + :chat-id from})) + (handlers/register-handler-fx :incoming-message (fn [_ [_ type {:keys [payload ttl id] :as message}]] @@ -320,31 +325,31 @@ :type type :ttl (+ (datetime/now-ms) ttl-s)} route-event (case type - :message [:received-protocol-message! message] - :group-message [:received-protocol-message! message] - :public-group-message [:received-protocol-message! message] - :ack (if (#{:message :group-message} (:type payload)) - [:update-message-status message :delivered] - [:pending-message-remove message]) - :seen [:update-message-status message :seen] - :group-invitation [:group-chat-invite-received message] - :update-group [:update-group-message message] - :add-group-identity [:participant-invited-to-group message] - :remove-group-identity [:participant-removed-from-group message] - :leave-group [:participant-left-group message] - :contact-request [:contact-request-received message] - :discover [:status-received message] - :discoveries-request [:discoveries-request-received message] - :discoveries-response [:discoveries-response-received message] - :profile [:contact-update-received message] - :update-keys [:update-keys-received message] - :online [:contact-online-received message] - :pending [:pending-message-upsert message] - :sent (let [{:keys [to id group-id]} message - message' {:from to - :payload {:message-id id - :group-id group-id}}] - [:update-message-status message' :sent]) + (:message + :group-message + :public-group-message) [:received-message (transform-protocol-message message)] + :ack (if (#{:message :group-message} (:type payload)) + [:update-message-status message :delivered] + [:pending-message-remove message]) + :seen [:update-message-status message :seen] + :group-invitation [:group-chat-invite-received message] + :update-group [:update-group-message message] + :add-group-identity [:participant-invited-to-group message] + :remove-group-identity [:participant-removed-from-group message] + :leave-group [:participant-left-group message] + :contact-request [:contact-request-received message] + :discover [:status-received message] + :discoveries-request [:discoveries-request-received message] + :discoveries-response [:discoveries-response-received message] + :profile [:contact-update-received message] + :update-keys [:update-keys-received message] + :online [:contact-online-received message] + :pending [:pending-message-upsert message] + :sent (let [{:keys [to id group-id]} message + message' {:from to + :payload {:message-id id + :group-id group-id}}] + [:update-message-status message' :sent]) nil)] (when (nil? route-event) (debug "Unknown message type" type)) (cache/add! processed-message) @@ -353,15 +358,20 @@ (when route-event {:dispatch route-event}))))))) (defn update-message-status [db {:keys [message-id ack-of-message group-id from status]}] - (let [message-id' (or ack-of-message message-id) - group? (boolean group-id) - status-path (if (and group? (not= status :sent)) - [:message-data :user-statuses message-id' from] - [:message-data :statuses message-id']) - {current-status :status} (get-in db status-path)] - (if-not (= :seen current-status) - (assoc-in db status-path {:whisper-identity from - :status status}) + (let [message-id' (or ack-of-message message-id) + update-group-status? (and group-id (not= status :sent)) + message-path [:chats (or group-id from) :messages message-id'] + current-status (if update-group-status? + (get-in db (into message-path [:user-statuses from :status])) + (get-in db (into message-path [:message-status])))] + ;; for some strange reason, we sometimes receive status update for message we don't have, + ;; that's why the first condition in if + (if (and (get-in db message-path) + (not= :seen current-status)) + (if update-group-status? + (assoc-in db (into message-path [:user-statuses from]) {:whisper-identity from + :status status}) + (assoc-in db (into message-path [:message-status]) status)) db))) (handlers/register-handler-fx @@ -503,4 +513,4 @@ (fn [_ [_ error]] (let [android-error? (re-find (re-pattern "Failed to connect") (.-message error))] (when android-error? - {::status-init-jail nil})))) \ No newline at end of file + {::status-init-jail nil})))) diff --git a/src/status_im/ui/components/chat_icon/screen.cljs b/src/status_im/ui/components/chat_icon/screen.cljs index fd2b368d0b..ec8e4c7e07 100644 --- a/src/status_im/ui/components/chat_icon/screen.cljs +++ b/src/status_im/ui/components/chat_icon/screen.cljs @@ -41,7 +41,7 @@ [view pending-inner-circle]]])) (defview chat-icon-view [chat-id group-chat name online styles & [hide-dapp?]] - [photo-path [:chat-photo chat-id] + [photo-path [:get-chat-photo chat-id] dapp? [:get-in [:contacts/contacts chat-id :dapp?]]] [view (:container styles) (if-not (s/blank? photo-path) diff --git a/src/status_im/ui/screens/accounts/login/events.cljs b/src/status_im/ui/screens/accounts/login/events.cljs index 4f2f91750c..55148879f7 100644 --- a/src/status_im/ui/screens/accounts/login/events.cljs +++ b/src/status_im/ui/screens/accounts/login/events.cljs @@ -28,13 +28,16 @@ (reg-fx ::change-account (fn [[address new-account?]] - (js/setTimeout - (fn [] - (data-store/change-account address new-account? - #(dispatch [:change-account-handler % address new-account?]))) - ;; if we don't add delay when running app without status-go - ;; "null is not an object (evaluating 'realm.schema')" error appears - (if config/stub-status-go? 300 0)))) + ;; if we don't add delay when running app without status-go + ;; "null is not an object (evaluating 'realm.schema')" error appears + (if config/stub-status-go? + (js/setTimeout + (fn [] + (data-store/change-account address new-account? + #(dispatch [:change-account-handler % address new-account?]))) + 300) + (data-store/change-account address new-account? + #(dispatch [:change-account-handler % address new-account?]))))) ;;;; Handlers diff --git a/src/status_im/ui/screens/chats_list/views/inner_item.cljs b/src/status_im/ui/screens/chats_list/views/inner_item.cljs index a03882fce7..e436d753d1 100644 --- a/src/status_im/ui/screens/chats_list/views/inner_item.cljs +++ b/src/status_im/ui/screens/chats_list/views/inner_item.cljs @@ -1,62 +1,57 @@ (ns status-im.ui.screens.chats-list.views.inner-item (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :refer [subscribe dispatch]] + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] [clojure.string :as str] - [status-im.ui.components.react :refer [view image text]] + [status-im.ui.components.react :as react] [status-im.ui.components.icons.vector-icons :as vi] - [status-im.ui.components.chat-icon.screen :refer [chat-icon-view-chat-list]] - [status-im.ui.components.context-menu :refer [context-menu]] + [status-im.ui.components.chat-icon.screen :as chat-icon-screen] + [status-im.ui.components.context-menu :as context-menu] [status-im.ui.screens.chats-list.styles :as st] - [status-im.utils.utils :refer [truncate-str]] - [status-im.i18n :refer [get-contact-translated label label-pluralize]] + [status-im.utils.utils :as utils] + [status-im.commands.utils :as commands-utils] + [status-im.i18n :as i18n] [status-im.utils.datetime :as time] - [status-im.utils.gfycat.core :refer [generate-gfy]] - [status-im.constants :refer [console-chat-id - content-type-command - content-type-wallet-command - content-type-command-request]] - [taoensso.timbre :as log] - [reagent.core :as r])) + [status-im.utils.gfycat.core :as gfycat] + [status-im.constants :as const] + [taoensso.timbre :as log])) -(defn message-content-text [chat-id] - (let [message (subscribe [:get-last-message chat-id]) - preview (subscribe [:get-last-message-short-preview chat-id])] - (r/create-class - {:display-name "message-content-text" - :component-will-mount - (fn [] - (when (and (get-in @message [:content :command]) - (not @preview)) - (dispatch [:request-command-message-data @message :short-preview]))) +(defn message-content-text [{:keys [content] :as message}] + (reagent/create-class + {:display-name "message-content-text" + :component-will-mount + #(when (and (or (:command content) + (:content-command content)) + (not (:short-preview content))) + (re-frame/dispatch [:request-command-message-data message + {:data-type :short-preview + :cache-data? true}])) + :reagent-render + (fn [{:keys [content] :as message}] + [react/view st/last-message-container + (cond - :reagent-render - (fn [_] - [view] - (let [{:keys [content] :as message} @message - preview @preview] - [view st/last-message-container - (cond + (not message) + [react/text {:style st/last-message-text} + (i18n/label :t/no-messages)] - (not message) - [text {:style st/last-message-text} - (label :t/no-messages)] + (str/blank? content) + [react/text {:style st/last-message-text} + ""] - (str/blank? content) - [text {:style st/last-message-text} - ""] + (:content content) + [react/text {:style st/last-message-text + :number-of-lines 1} + (:content content)] - (:content content) - [text {:style st/last-message-text - :number-of-lines 1} - (:content content)] + (and (:command content) + (-> content :short-preview :markup)) + (commands-utils/generate-hiccup (-> content :short-preview :markup)) - (:command content) - preview - - :else - [text {:style st/last-message-text - :number-of-lines 1} - content])]))}))) + :else + [react/text {:style st/last-message-text + :number-of-lines 1} + content])])})) (defview message-status [{:keys [chat-id contacts]} {:keys [message-id message-status user-statuses message-type outgoing] :as msg}] @@ -70,29 +65,29 @@ (and (= (count user-statuses) (count contacts)) (every? (fn [[_ {:keys [status]}]] (= (keyword status) :seen)) user-statuses))) - (= chat-id console-chat-id))) - [image {:source {:uri :icon_ok_small} - :style st/status-image}])))) + (= chat-id const/console-chat-id))) + [react/image {:source {:uri :icon_ok_small} + :style st/status-image}])))) (defn message-timestamp [{:keys [timestamp]}] (when timestamp - [text {:style st/datetime-text} + [react/text {:style st/datetime-text} (time/to-short-str timestamp)])) (defview unviewed-indicator [chat-id] - (letsubs [unviewed-messages [:unviewed-messages-count chat-id]] - (when (pos? unviewed-messages) - [view st/new-messages-container - [text {:style st/new-messages-text - :font :medium} - unviewed-messages]]))) + (letsubs [unviewed-messages-count [:unviewed-messages-count chat-id]] + (when (pos? unviewed-messages-count) + [react/view st/new-messages-container + [react/text {:style st/new-messages-text + :font :medium} + unviewed-messages-count]]))) (defn options-btn [chat-id] - (let [options [{:value #(dispatch [:remove-chat chat-id]) - :text (label :t/delete-chat) + (let [options [{:value #(re-frame/dispatch [:remove-chat chat-id]) + :text (i18n/label :t/delete-chat) :destructive? true}]] - [view st/opts-btn-container - [context-menu + [react/view st/opts-btn-container + [context-menu/context-menu [vi/icon :icons/options] options nil @@ -102,41 +97,41 @@ (let [private-group? (and group-chat? (not public?)) public-group? (and group-chat? public?) chat-name (if (str/blank? name) - (generate-gfy public-key) - (truncate-str name 30))] - [view st/name-view + (gfycat/generate-gfy public-key) + (utils/truncate-str name 30))] + [react/view st/name-view (when public-group? - [view st/public-group-icon-container + [react/view st/public-group-icon-container [vi/icon :icons/public-chat {:style st/public-group-icon}]]) (when private-group? - [view st/private-group-icon-container - [vi/icon :icons/group-chat {:style st/private-group-icon}]]) - [view {:flex-shrink 1} - [text {:style st/name-text - :number-of-lines 1} + [react/view st/private-group-icon-container + [vi/icon :icons/group-chat {:style st/private-group-icon}]]) + [react/view {:flex-shrink 1} + [react/text {:style st/name-text + :number-of-lines 1} (if public-group? (str "#" chat-name) chat-name)]]])) -(defn chat-list-item-inner-view [{:keys [chat-id name color online - group-chat contacts public? - public-key unremovable?] :as chat} - edit?] - (let [last-message (subscribe [:get-last-message chat-id]) - name (or (get-contact-translated chat-id :name name) - (generate-gfy public-key))] - [view st/chat-container - [view st/chat-icon-container - [chat-icon-view-chat-list chat-id group-chat name color online]] - [view st/chat-info-container - [view st/item-upper-container - [chat-list-item-name name group-chat public? public-key] - (when (and (not edit?) @last-message) - [view st/message-status-container - [message-status chat @last-message] - [message-timestamp @last-message]])] - [view st/item-lower-container - [message-content-text chat-id] - (when-not edit? [unviewed-indicator chat-id])]] - [view st/chat-options-container - (when (and edit? (not unremovable?)) [options-btn chat-id])]])) +(defview chat-list-item-inner-view [{:keys [chat-id name color online + group-chat contacts public? + public-key unremovable?] :as chat} + edit?] + (letsubs [last-message [:get-last-message chat-id]] + (let [name (or (i18n/get-contact-translated chat-id :name name) + (gfycat/generate-gfy public-key))] + [react/view st/chat-container + [react/view st/chat-icon-container + [chat-icon-screen/chat-icon-view-chat-list chat-id group-chat name color online]] + [react/view st/chat-info-container + [react/view st/item-upper-container + [chat-list-item-name name group-chat public? public-key] + (when (and (not edit?) last-message) + [react/view st/message-status-container + [message-status chat last-message] + [message-timestamp last-message]])] + [react/view st/item-lower-container + [message-content-text last-message] + (when-not edit? [unviewed-indicator chat-id])]] + [react/view st/chat-options-container + (when (and edit? (not unremovable?)) [options-btn chat-id])]]))) diff --git a/src/status_im/ui/screens/contacts/subs.cljs b/src/status_im/ui/screens/contacts/subs.cljs index a89afaeafd..5cbd82a733 100644 --- a/src/status_im/ui/screens/contacts/subs.cljs +++ b/src/status_im/ui/screens/contacts/subs.cljs @@ -153,11 +153,6 @@ (fn [contacts [_ identity]] (:name (contacts identity)))) -(reg-sub :chat-by-id - :<- [:chats] - (fn [chats [_ chat-id]] - (get chats chat-id))) - (defn chat-contacts [[chat contacts] [_ fn]] (when chat (let [current-participants (->> chat @@ -184,15 +179,13 @@ (reg-sub :contacts-by-chat (fn [[_ fn chat-id] _] - [(subscribe [:chat-by-id chat-id]) + [(subscribe [:get-chat chat-id]) (subscribe [:get-contacts])]) chat-contacts) -(reg-sub :chat-photo +(reg-sub :get-chat-photo (fn [[_ chat-id] _] - [(if chat-id - (subscribe [:chat-by-id chat-id]) - (subscribe [:get-current-chat])) + [(subscribe [:get-chat chat-id]) (subscribe [:contacts-by-chat filter chat-id])]) (fn [[chat contacts] [_ chat-id]] (when (and chat (not (:group-chat chat))) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 210520fe91..93bb2bebf6 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -25,8 +25,7 @@ :group/contact-groups {} :group/selected-contacts #{} :chats {} - :current-chat-id constants/console-chat-id - :loading-allowed true + :current-chat-id constants/console-chat-id :selected-participants #{} :discoveries {} :discover-search-tags '() @@ -153,19 +152,15 @@ :chat/chat-list-ui-props :chat/layout-height :chat/expandable-view-height-to-value - :chat/loading-allowed :chat/message-data - :chat/message-id->transaction-id - :chat/message-status - :chat/unviewed-messages + :chat/message-status :chat/selected-participants - :chat/chat-loaded-callbacks + :chat/chat-loaded-callbacks :chat/command-hash-valid? :chat/public-group-topic :chat/confirmation-code-sms-listener - :chat/messages - :chat/loaded-chats - :chat/raw-unviewed-messages + :chat/messages + :chat/loaded-chats :chat/bot-db :chat/geolocation :commands/access-scope->commands-responses 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 ed738a9fc2..c5d8446238 100644 --- a/src/status_im/ui/screens/group/chat_settings/events.cljs +++ b/src/status_im/ui/screens/group/chat_settings/events.cljs @@ -171,7 +171,5 @@ (register-handler-fx :clear-history (fn [{{:keys [current-chat-id] :as db} :db} _] - {:db (-> db - (assoc-in [:chats current-chat-id :messages] '()) - (assoc-in [:chats current-chat-id :last-message] nil)) + {:db (assoc-in db [:chats current-chat-id :messages] {}) ::chat-events/delete-messages current-chat-id})) diff --git a/src/status_im/ui/screens/profile/db.cljs b/src/status_im/ui/screens/profile/db.cljs index 5187d1b3bd..65a7e9cc83 100644 --- a/src/status_im/ui/screens/profile/db.cljs +++ b/src/status_im/ui/screens/profile/db.cljs @@ -13,8 +13,7 @@ (every? false? [(string/blank? username) (homoglyph/matches username constants/console-chat-id) - (string/includes? username chat.constants/command-char) - (string/includes? username chat.constants/bot-char)]))) + (string/includes? username chat.constants/command-char)]))) (defn correct-email? [email] (let [pattern #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"] diff --git a/src/status_im/ui/screens/profile/events.cljs b/src/status_im/ui/screens/profile/events.cljs index f56d2e7d61..440211c7fe 100644 --- a/src/status_im/ui/screens/profile/events.cljs +++ b/src/status_im/ui/screens/profile/events.cljs @@ -32,7 +32,7 @@ (handlers/register-handler-fx :profile/send-transaction - [re-frame/trim-v (re-frame/inject-cofx :get-stored-messages)] + [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)] (-> (chat-events/navigate-to-chat cofx chat-id) diff --git a/test/cljs/status_im/test/chat/events.cljs b/test/cljs/status_im/test/chat/events.cljs index 4b4be0c0a8..d3483dc60d 100644 --- a/test/cljs/status_im/test/chat/events.cljs +++ b/test/cljs/status_im/test/chat/events.cljs @@ -30,8 +30,8 @@ (is (= (:current-chat-id db) const/console-chat-id)) (is (= dispatch-n - (concat [[:add-contacts [sign-up/console-contact]]] - sign-up/intro-events))))) + [[:add-contacts [sign-up/console-contact]] + sign-up/intro-event])))) (testing "initialising console with existing account and console chat not initialisated" (let [fresh-db {:chats {} diff --git a/test/cljs/status_im/test/chat/models/input.cljs b/test/cljs/status_im/test/chat/models/input.cljs index 029b0ff5dd..1b9b568c39 100644 --- a/test/cljs/status_im/test/chat/models/input.cljs +++ b/test/cljs/status_im/test/chat/models/input.cljs @@ -46,10 +46,10 @@ (is (= "word1 \uD83D\uDC4D word2" (input/text->emoji "word1 :+1: word2")))) (deftest starts-as-command? - (is (false? (input/starts-as-command? nil))) - (is (false? (input/text-ends-with-space? ""))) - (is (false? (input/text-ends-with-space? "word1 word2 word3"))) - (is (true? (input/text-ends-with-space? "word1 word2 ")))) + (is (not (input/starts-as-command? nil))) + (is (not (input/text-ends-with-space? ""))) + (is (not (input/text-ends-with-space? "word1 word2 word3"))) + (is (input/text-ends-with-space? "word1 word2 "))) (deftest split-command-args (is (nil? (input/split-command-args nil)))