Refactored message data-model and view

This commit is contained in:
janherich 2017-12-05 14:03:25 +01:00
parent df21ec8c3a
commit eb8d0a8a79
No known key found for this signature in database
GPG Key ID: C23B473AFBE94D13
38 changed files with 916 additions and 1110 deletions

View File

@ -3,7 +3,6 @@
(def command-char "/")
(def spacing-char " ")
(def arg-wrapping-char "\"")
(def bot-char "@")
(def input-height 56)
(def max-input-height 66)

View File

@ -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))]
(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
(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}))))
(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)

View File

@ -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]]
(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-> {}
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)))))
(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)))))))

View File

@ -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
(request-command-data
db
(merge params-template
{:data-type :preview
:event-after-creator (fn [command-message _]
[::send-command command-message])})))))
:event-after-creator (fn [command-message returned]
[::send-command (assoc-in command-message
[:command :preview] returned)])})))))
(handlers/register-handler-fx
:send-current-message

View File

@ -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)]
{: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))
(when (and (not (message-exists? message-id))
(not= from public-key)
(pop-up-chat? chat-identifier))
(let [group-chat? (not (nil? group-id))
chat-exists? (get-in db [:chats chat-identifier])
fx (if chat-exists?
(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)
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]}]})))

View File

@ -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"))

View File

@ -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,9 +168,7 @@
(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)
message' {:message-id (random/id)
:chat-id chat-id
:content message
:from identity
@ -178,7 +176,7 @@
:outgoing true
:timestamp (datetime/now-ms)
:clock-value (clocks/send clock-value)
:show? true})
:show? true}
message'' (cond-> message'
(and group-chat public?)
(assoc :group-id chat-id :message-type :public-group-user-message)

View File

@ -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.

View File

@ -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)))
(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))

View File

@ -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]))
(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 (to-datasource-inverted messages)}]))
:dataSource (listview/to-datasource-inverted messages)}]))
(defview chat []
[group-chat [:chat :group-chat]
(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]
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
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)
(dispatch [:set-layout-height height]))))}
(re-frame/dispatch [:set-layout-height height]))))}
[chat-toolbar]
[messages-view group-chat]
[input/container {:text-empty? (str/blank? input-text)}]
[input/container {:text-empty? (string/blank? input-text)}]
(when show-actions?
[actions-view])
[actions/actions-view])
(when show-bottom-info?
[bottom-info-view])
[offline-view {:top (get-in platform-specific
[:component-styles :status-bar :default :height])}]])
[bottom-info/bottom-info-view])
[offline/offline-view {:top (get-in platform/platform-specific
[:component-styles :status-bar :default :height])}]]))

View File

@ -120,19 +120,8 @@
: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]
(def intro-event
[: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"
@ -140,7 +129,7 @@
:content-type const/content-type-command-request
:outgoing false
:from const/console-chat-id
:to "me"}]])
: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)})

View File

@ -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/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/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/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/messages (s/nilable map?)) ; messages indexed by message-id
(s/def :chat/loaded-chats (s/nilable seq?))
(s/def :chat/raw-unviewed-messages (s/nilable vector?))
(s/def :chat/bot-db (s/nilable map?))
(s/def :chat/geolocation (s/nilable map?))

View File

@ -1,41 +1,32 @@
(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
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
@ -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)})

View File

@ -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))))

View File

@ -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))

View File

@ -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))

View File

@ -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]]
(:require [status-im.ui.components.react :as react]
[clojure.string :as str]
[status-im.i18n :refer [label]]
[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)))]]])
[react/view st/datemark-wrapper
[react/view st/datemark
[react/text {:style st/datemark-text}
(str/capitalize value)]]])

View File

@ -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]
(letsubs [current-account [:get-current-account]
incoming-name [:contact-name-by-identity from]]
(if-let [name (if outgoing
(when-let [name (if outgoing
(:name current-account)
(or incoming-name "Unknown contact"))]
[text {:style st/author} name]))
[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]
(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-chat
{:photo-path nil
:status nil
:last-online 0}
(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
[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 (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
(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}
(or status content)])
(if (> messages-count 1)
[view st/message-datemark
[chat-datemark datemark]]
[view st/message-empty-spacing])])))
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
[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
[{: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}
(str chat-consts/command-char name)]]])
(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,12 +99,11 @@
{"\\*[^*]+\\*" {: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)
(into [] (comp (map first)
(map #(vector % (re-pattern %)))
(drop-while (fn [[_ regx]] (not (re-matches regx string))))
(take 1)))
@ -171,14 +113,13 @@
;; 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]
styled-text (vec (map-indexed (fn [idx string]
(let [style (get-style string)]
[text
[react/text
{:key (str idx "_" string)
:style style}
(subs string 1 (dec (count string)))]))
@ -197,41 +138,31 @@
simple-text? (and (= (count parsed-text) 2)
(nil? (second parsed-text)))]
(if simple-text?
[autolink {:style (st/text-message message)
[react/autolink {:style (style/text-message message)
:text (apply str parsed-text)
:onPress #(dispatch [:browse-link-from-message %])}]
[text {:style (st/text-message message)} 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,98 +175,93 @@
: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]
(letsubs [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)
(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?)
[view st/delivery-view
[text {:style st/delivery-text
[react/view style/delivery-view
[react/text {:style style/delivery-text
:font :default}
(message-status-label
(i18n/message-status-label
(if seen-by-everyone?
:seen-by-everyone
status))]]
[touchable-highlight
[react/touchable-highlight
{:on-press (fn []
(dispatch [:show-message-details {:message-status status
(re-frame/dispatch [:show-message-details {:message-status status
:user-statuses user-statuses
:participants participants}]))}
[view st/delivery-view
[react/view style/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))}
[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)
[text {:style st/delivery-text
[react/text {:style style/delivery-text
:font :default}
(str "+ " (- (count user-statuses) 3))])]])))
(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
(or delivery-status message-status :sending))]
[react/view style/delivery-view
[react/text {:style style/delivery-text
:font :default}
(message-status-label status)]]))
(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)
(letsubs [photo-path [:photo-path from]]
[react/view
[react/image {:source {:uri (if (string/blank? photo-path)
(identicon/identicon from)
photo-path)}
:style st/photo}]])
:style style/photo}]]))
(defview my-photo [from]
[account [:get-current-account]]
(letsubs [account [:get-current-account]]
(let [{:keys [photo-path]} account]
[view
[image {:source {:uri (if (str/blank? photo-path)
(identicon from)
[react/view
[react/image {:source {:uri (if (string/blank? photo-path)
(identicon/identicon from)
photo-path)}
:style st/photo}]]))
: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))
[{: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]))]
[view (st/group-message-view message)
[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]))]]]))
[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
(animation/start
(animation/timing val {:toValue to-value
:duration 250})
(fn [arg]
(when (.-finished arg)
@ -343,63 +269,56 @@
(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)
[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
(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-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
#(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}])))
: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
[react/touchable-highlight {:on-press #(when platform/ios?
(re-frame/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)))
(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] (str/split address #"&amp;")]
(share-or-open-map location lat long)))}
[view
[location lat long] (string/split address #"&amp;")]
(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})])]]])})))
{:incoming-group incoming-group})])]]])}))

View File

@ -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

View File

@ -14,8 +14,6 @@
(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-status "status")
(def min-password-length 6)

View File

@ -20,9 +20,7 @@
(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))))
(data-store/save chat (data-store/exists? chat-id)))
(defn delete
[chat-id]

View File

@ -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]
(if (string? content)
content
(pr-str
(update content :params dissoc :password :password-confirmation)))
;; 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]

View File

@ -17,9 +17,6 @@
(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))
([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))))

View File

@ -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}}})

View File

@ -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]
(: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.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]
@ -29,6 +29,13 @@
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")
@ -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"))

View File

@ -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}}})

View File

@ -33,7 +33,7 @@
(cond
(= path [:responses "password" :preview])
(callback {:result {:context {},
:messages [],
:messages {},
:returned {:markup ["text"
{:style
{:color "black",

View File

@ -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,9 +325,9 @@
: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]
(: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])
@ -354,14 +359,19 @@
(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
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

View File

@ -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)

View File

@ -28,13 +28,16 @@
(reg-fx
::change-account
(fn [[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?
(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))))
300)
(data-store/change-account address new-account?
#(dispatch [:change-account-handler % address new-account?])))))
;;;; Handlers

View File

@ -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
(defn message-content-text [{:keys [content] :as message}]
(reagent/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])))
#(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 [_]
[view]
(let [{:keys [content] :as message} @message
preview @preview]
[view st/last-message-container
(fn [{:keys [content] :as message}]
[react/view st/last-message-container
(cond
(not message)
[text {:style st/last-message-text}
(label :t/no-messages)]
[react/text {:style st/last-message-text}
(i18n/label :t/no-messages)]
(str/blank? content)
[text {:style st/last-message-text}
[react/text {:style st/last-message-text}
""]
(:content content)
[text {:style st/last-message-text
[react/text {:style st/last-message-text
:number-of-lines 1}
(:content content)]
(:command content)
preview
(and (:command content)
(-> content :short-preview :markup))
(commands-utils/generate-hiccup (-> content :short-preview :markup))
:else
[text {:style st/last-message-text
[react/text {:style st/last-message-text
:number-of-lines 1}
content])]))})))
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}
(= 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
(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]])))
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
[react/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
[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
(defview 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
(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)
[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 (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])]]
[view st/chat-options-container
(when (and edit? (not unremovable?)) [options-btn chat-id])]]))
[react/view st/chat-options-container
(when (and edit? (not unremovable?)) [options-btn chat-id])]])))

View File

@ -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)))

View File

@ -26,7 +26,6 @@
:group/selected-contacts #{}
:chats {}
:current-chat-id constants/console-chat-id
:loading-allowed true
:selected-participants #{}
:discoveries {}
:discover-search-tags '()
@ -153,11 +152,8 @@
: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/selected-participants
:chat/chat-loaded-callbacks
:chat/command-hash-valid?
@ -165,7 +161,6 @@
:chat/confirmation-code-sms-listener
:chat/messages
:chat/loaded-chats
:chat/raw-unviewed-messages
:chat/bot-db
:chat/geolocation
:commands/access-scope->commands-responses

View File

@ -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}))

View File

@ -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])?"]

View File

@ -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)

View File

@ -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 {}

View File

@ -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)))