feat: deleted for me message UI (#14168)

* feat: delete for me message UI

delete and sync deleted for me messages immediately after leaving chat
view

Signed-off-by: yqrashawn <namy.19@gmail.com>

* fix: system message width/height

Signed-off-by: yqrashawn <namy.19@gmail.com>

Signed-off-by: yqrashawn <namy.19@gmail.com>
This commit is contained in:
yqrashawn 2022-10-21 13:12:40 +08:00 committed by GitHub
parent 1b6eaec719
commit 0cbd3ec805
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 151 deletions

View File

@ -1,14 +1,13 @@
(ns quo2.components.messages.system-message (ns quo2.components.messages.system-message
(:require [status-im.i18n.i18n :as i18n] (:require [quo.react-native :as rn]
[quo.react-native :as rn]
[status-im.utils.core :as utils]
[quo.theme :as theme] [quo.theme :as theme]
[quo2.components.buttons.button :as button] [quo2.components.avatars.icon-avatar :as icon-avatar]
[quo2.components.markdown.text :as text]
[quo2.reanimated :as ra]
[quo2.foundations.colors :as colors]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar :as user-avatar]
[quo2.components.avatars.icon-avatar :as icon-avatar])) [quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[quo2.reanimated :as ra]
[status-im.i18n.i18n :as i18n]
[status-im.utils.core :as utils]))
(def themes-landed {:pinned colors/primary-50-opa-5 (def themes-landed {:pinned colors/primary-50-opa-5
:added colors/primary-50-opa-5 :added colors/primary-50-opa-5
@ -55,7 +54,7 @@
(defmulti sm-render :type) (defmulti sm-render :type)
(defmethod sm-render :deleted [{:keys [state action timestamp-str]}] (defmethod sm-render :deleted [{:keys [label timestamp-str]}]
[rn/view {:align-items :center [rn/view {:align-items :center
:justify-content :space-between :justify-content :space-between
:flex 1 :flex 1
@ -64,15 +63,12 @@
:flex-direction :row} :flex-direction :row}
[sm-icon {:icon :main-icons/delete16 [sm-icon {:icon :main-icons/delete16
:color :danger :color :danger
:opacity (if (= state :landed) 0 5)}] :opacity 5}]
[text/text {:size :paragraph-2 [text/text {:size :paragraph-2
:style {:color (get-color :text) :style {:color (get-color :text)
:margin-right 5}} :margin-right 5}}
(i18n/label (if action :message-deleted-for-you :message-deleted))] (i18n/label (or label :message-deleted))]
(when (nil? action) [sm-timestamp timestamp-str])] [sm-timestamp timestamp-str]]])
(when action [button/button {:size 24
:before :main-icons/timeout
:type :grey} (i18n/label :undo)])])
(defmethod sm-render :added [{:keys [state mentions timestamp-str]}] (defmethod sm-render :added [{:keys [state mentions timestamp-str]}]
[rn/view {:align-items :center [rn/view {:align-items :center
@ -140,23 +136,27 @@
:style {:color (get-color :time)}} :style {:color (get-color :time)}}
(utils/truncate-str (:info content) 24)])]]]]) (utils/truncate-str (:info content) 24)])]]]])
(defn system-message [{:keys [type] :as message}] (defn system-message
[{:keys [type style non-pressable? animate-landing?] :as message}]
[:f> [:f>
(fn [] (fn []
(let [sv-color (ra/use-shared-value (get-color :bg :landed type))] (let [sv-color (ra/use-shared-value
(ra/animate-shared-value-with-delay (get-color :bg (if animate-landing? :landed :default) type))]
sv-color (get-color :bg :default type) 0 :linear 1000) (when animate-landing?
(ra/animate-shared-value-with-delay
sv-color (get-color :bg :default type) 0 :linear 1000))
[ra/touchable-opacity [ra/touchable-opacity
{:on-press #(ra/set-shared-value {:on-press #(when-not non-pressable?
sv-color (get-color :bg :pressed type)) (ra/set-shared-value
sv-color (get-color :bg :pressed type)))
:style (ra/apply-animations-to-style :style (ra/apply-animations-to-style
{:background-color sv-color} {:background-color sv-color}
{:flex-direction :row (merge
:flex 1 {:flex-direction :row
:border-radius 16 :flex 1
:padding-vertical 9 :border-radius 16
:padding-horizontal 11 :padding-vertical 9
:width 359 :padding-horizontal 11
:height 52 :background-color sv-color}
:background-color sv-color})} style))}
[sm-render message]]))]) [sm-render message]]))])

View File

@ -1,5 +1,6 @@
(ns status-im.chat.models.delete-message-for-me (ns status-im.chat.models.delete-message-for-me
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.chat.models.message-list :as message-list]
[status-im.ethereum.json-rpc :as json-rpc] [status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.datetime :as datetime] [status-im.utils.datetime :as datetime]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
@ -43,17 +44,22 @@
{:events [:chat.ui/delete-message-for-me]} {:events [:chat.ui/delete-message-for-me]}
[{:keys [db]} {:keys [chat-id message-id]} undo-time-limit-ms] [{:keys [db]} {:keys [chat-id message-id]} undo-time-limit-ms]
(when (get-in db [:messages chat-id message-id]) (when (get-in db [:messages chat-id message-id])
{:db (update-db-delete-locally db chat-id message-id undo-time-limit-ms) (assoc
(message-list/rebuild-message-list
{:db (update-db-delete-locally db chat-id message-id undo-time-limit-ms)}
chat-id)
:utils/dispatch-later [{:dispatch [:chat.ui/delete-message-for-me-and-sync :utils/dispatch-later [{:dispatch [:chat.ui/delete-message-for-me-and-sync
{:chat-id chat-id {:chat-id chat-id
:message-id message-id}] :message-id message-id}]
:ms undo-time-limit-ms}]})) :ms undo-time-limit-ms}])))
(fx/defn undo (fx/defn undo
{:events [:chat.ui/undo-delete-message-for-me]} {:events [:chat.ui/undo-delete-message-for-me]}
[{:keys [db]} {:keys [chat-id message-id]}] [{:keys [db]} {:keys [chat-id message-id]}]
(when (get-in db [:messages chat-id message-id]) (when (get-in db [:messages chat-id message-id])
{:db (update-db-undo-locally db chat-id message-id)})) (message-list/rebuild-message-list
{:db (update-db-undo-locally db chat-id message-id)}
chat-id)))
(fx/defn delete-and-sync (fx/defn delete-and-sync
{:events [:chat.ui/delete-message-for-me-and-sync]} {:events [:chat.ui/delete-message-for-me-and-sync]}
@ -66,3 +72,19 @@
:on-error #(log/error "failed to delete message for me " %) :on-error #(log/error "failed to delete message for me " %)
:on-success #(re-frame/dispatch [:sanitize-messages-and-process-response :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response
%])}]})) %])}]}))
(defn- chats-reducer
"traverse all messages find not yet synced deleted-for-me? messages, generate dispatch vector"
[acc chat-id messages]
(reduce-kv
(fn [inner-acc message-id {:keys [deleted-for-me? deleted-for-me-undoable-till]}]
(if (and deleted-for-me? deleted-for-me-undoable-till)
(conj inner-acc [:chat.ui/delete-message-for-me-and-sync chat-id message-id])
inner-acc))
acc
messages))
(fx/defn sync-all
"Get all deleted-for-me messages that not yet synced with status-go and sync them"
{:events [:chat.ui/sync-all-deleted-for-me-messages]}
[{:keys [db]}]
{:dispatch-n (reduce-kv chats-reducer [] (:messages db))})

View File

@ -8,65 +8,47 @@
(defonce cid "chat-id") (defonce cid "chat-id")
(deftest delete-for-me (deftest delete-for-me
(let [db {:messages {cid {mid {:id mid}}}} (with-redefs [datetime/timestamp (constantly 1)]
message {:message-id mid :chat-id cid}] (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}}
(testing "delete for me" message {:message-id mid :chat-id cid}]
(let [expected {:db {:messages {"chat-id" {"message-id" (testing "delete for me"
{:id "message-id" (let [result-message (get-in (delete-message-for-me/delete {:db db} message 1000)
:deleted-for-me? true}}}} [:db :messages cid mid])]
:utils/dispatch-later (is (= (:id result-message) mid))
[{:dispatch [:chat.ui/delete-message-for-me-and-sync (is (true? (:deleted-for-me? result-message)))
{:chat-id "chat-id" :message-id "message-id"}] (is (= (:deleted-for-me-undoable-till result-message) 1001))))
:ms 1000}]} (testing "should return nil if message not in db"
result (delete-message-for-me/delete {:db db} message 1000) (is (= (delete-message-for-me/delete {:db {:messages []}} message 1000)
timestamp (+ (datetime/timestamp) 1000)] nil))))))
(is (= (update-in result [:db :messages "chat-id" "message-id"] dissoc :deleted-for-me-undoable-till)
expected))
(is (-> (get-in result [:db :messages "chat-id" "message-id" :deleted-for-me-undoable-till])
(- timestamp)
js/Math.abs
(< 10)))))
(testing "should return nil if message in db"
(is (= (delete-message-for-me/delete {:db {:messages []}} message 1000)
nil)))))
(deftest undo-delete-for-me (deftest undo-delete-for-me
(let [db {:messages {cid {mid {:id mid}}}} (let [db {:messages {cid {mid {:id mid :whisper-timestamp 1}}}}
message {:message-id mid :chat-id cid}] message {:message-id mid :chat-id cid}]
(testing "undo delete for me in time" (testing "undo delete for me in time"
(let [db (update-in db (let [db (update-in db
[:messages cid mid] [:messages cid mid]
assoc assoc
:deleted-for-me? true :deleted-for-me? true
:deleted-for-me-undoable-till :deleted-for-me-undoable-till
(+ (datetime/timestamp) 1000)) (+ (datetime/timestamp) 1000))
result-message (get-in (delete-message-for-me/undo {:db db} message)
[:db :messages cid mid])]
(is (= (:id result-message) mid))
(is (nil? (:deleted-for-me? result-message)))
(is (nil? (:deleted-for-me-undoable-till result-message)))))
expected {:db {:messages {"chat-id" {"message-id"
{:id "message-id"}}}}}]
(is (= (delete-message-for-me/undo {:db db} message) expected))))
(testing "remain deleted for me when undo delete for me late" (testing "remain deleted for me when undo delete for me late"
(let [db (update-in db (let [db (update-in db
[:messages cid mid] [:messages cid mid]
assoc assoc
:deleted-for-me? true :deleted-for-me? true
:deleted-for-me-undoable-till (- (datetime/timestamp) 1000)) :deleted-for-me-undoable-till (- (datetime/timestamp) 1000))
result-message (get-in (delete-message-for-me/undo {:db db} message) [:db :messages cid mid])]
(is (= (:id result-message) mid))
(is (nil? (:deleted-for-me-undoable-till result-message)))
(is (true? (:deleted-for-me? result-message)))))
expected {:db {:messages {"chat-id" {"message-id" (testing "should return nil if message not in db"
{:id "message-id"
:deleted-for-me? true}}}}}]
(is (= (delete-message-for-me/undo {:db db} message) expected))))
(testing "remain deleted for me when undo delete for me late"
(let [db (update-in db
[:messages cid mid]
assoc
:deleted-for-me? true
:deleted-for-me-undoable-till (- (datetime/timestamp) 1000))
expected {:db {:messages {"chat-id" {"message-id"
{:id "message-id"
:deleted-for-me? true}}}}}]
(is (= (delete-message-for-me/undo {:db db} message) expected))))
(testing "should return nil if message in db"
(is (= (delete-message-for-me/undo {:db {:messages []}} message) (is (= (delete-message-for-me/undo {:db {:messages []}} message)
nil))))) nil)))))
@ -74,7 +56,7 @@
(let [db {:messages {cid {mid {:id mid}}}} (let [db {:messages {cid {mid {:id mid}}}}
message {:message-id mid :chat-id cid}] message {:message-id mid :chat-id cid}]
(testing "delete for me and sync" (testing "delete for me and sync"
(let [expected-db {:messages {"chat-id" {"message-id" {:id "message-id"}}}} (let [expected-db {:messages {cid {mid {:id mid}}}}
effects (delete-message-for-me/delete-and-sync {:db db} message) effects (delete-message-for-me/delete-and-sync {:db db} message)
result-db (:db effects) result-db (:db effects)
rpc-calls (:status-im.ethereum.json-rpc/call effects)] rpc-calls (:status-im.ethereum.json-rpc/call effects)]
@ -100,7 +82,7 @@
second) second)
mid)))) mid))))
(testing "delete for me and sync, should clean undo timer" (testing "delete for me and sync, should clean undo timer"
(let [expected-db {:messages {"chat-id" {"message-id" {:id "message-id"}}}} (let [expected-db {:messages {cid {mid {:id mid}}}}
effects (delete-message-for-me/delete-and-sync effects (delete-message-for-me/delete-and-sync
{:db (update-in db {:db (update-in db
[:messages cid mid [:messages cid mid
@ -109,7 +91,7 @@
message) message)
result-db (:db effects)] result-db (:db effects)]
(is (= result-db expected-db)))) (is (= result-db expected-db))))
(testing "should return nil if message in db" (testing "should return nil if message not in db"
(is (= (delete-message-for-me/delete-and-sync {:db {:messages []}} (is (= (delete-message-for-me/delete-and-sync {:db {:messages []}}
message) message)
nil))))) nil)))))

View File

@ -1,8 +1,8 @@
(ns status-im.chat.models.message-list (ns status-im.chat.models.message-list
(:require [status-im.constants :as constants] (:require ["functional-red-black-tree" :as rb-tree]
[status-im.utils.fx :as fx] [status-im.constants :as constants]
[status-im.utils.datetime :as time] [status-im.utils.datetime :as time]
["functional-red-black-tree" :as rb-tree])) [status-im.utils.fx :as fx]))
(defn- add-datemark [{:keys [whisper-timestamp] :as msg}] (defn- add-datemark [{:keys [whisper-timestamp] :as msg}]
;;TODO this is slow ;;TODO this is slow
@ -16,16 +16,20 @@
message-type message-type
from from
outgoing outgoing
whisper-timestamp]}] whisper-timestamp
deleted-for-me?]}]
(-> {:whisper-timestamp whisper-timestamp (-> {:whisper-timestamp whisper-timestamp
:from from :from from
:one-to-one? (= constants/message-type-one-to-one message-type) :one-to-one? (= constants/message-type-one-to-one message-type)
:system-message? (= constants/message-type-private-group-system-message :system-message? (boolean
message-type) (or
:clock-value clock-value (= constants/message-type-private-group-system-message
:type :message message-type)
:message-id message-id deleted-for-me?))
:outgoing (boolean outgoing)} :clock-value clock-value
:type :message
:message-id message-id
:outgoing (boolean outgoing)}
add-datemark add-datemark
add-timestamp)) add-timestamp))

View File

@ -1,21 +1,22 @@
(ns status-im.navigation.core (ns status-im.navigation.core
(:require (:require
["react-native" :as rn] ["react-native" :as rn]
[clojure.set :as clojure.set]
["react-native-gesture-handler" :refer (gestureHandlerRootHOC)] ["react-native-gesture-handler" :refer (gestureHandlerRootHOC)]
["react-native-navigation" :refer (Navigation)] ["react-native-navigation" :refer (Navigation)]
[clojure.set :as clojure.set]
[quo.components.text-input :as quo.text-input] [quo.components.text-input :as quo.text-input]
[quo.design-system.colors :as quo.colors] [quo.design-system.colors :as quo.colors]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.multiaccounts.login.core :as login-core]
[status-im.navigation.roots :as roots] [status-im.navigation.roots :as roots]
[status-im.navigation.state :as state]
[status-im.ui.components.icons.icons :as icons] [status-im.ui.components.icons.icons :as icons]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.screens.views :as views] [status-im.ui.screens.views :as views]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[taoensso.timbre :as log] [taoensso.encore :as enc]
[status-im.multiaccounts.login.core :as login-core] [taoensso.timbre :as log]))
[status-im.navigation.state :as state]))
(def debug? ^boolean js/goog.DEBUG) (def debug? ^boolean js/goog.DEBUG)
@ -438,4 +439,6 @@
:community :community
:else :else
:home))})) :home))
:dispatch-n (enc/conj-when []
(and (= view-id :chat) [:chat.ui/sync-all-deleted-for-me-messages]))}))

View File

@ -1,6 +1,10 @@
(ns status-im.ui2.screens.chat.messages.message (ns status-im.ui2.screens.chat.messages.message
(:require [quo.core :as quo] (:require [quo.core :as quo]
[quo.design-system.colors :as colors] [quo.design-system.colors :as colors]
[quo.react-native :as rn]
[quo2.components.messages.system-message :as system-message]
[quo2.foundations.colors :as quo2.colors]
[quo2.foundations.typography :as typography]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.chat.models.delete-message-for-me] [status-im.chat.models.delete-message-for-me]
@ -11,11 +15,12 @@
[status-im.i18n.i18n :as i18n] [status-im.i18n.i18n :as i18n]
[status-im.react-native.resources :as resources] [status-im.react-native.resources :as resources]
[status-im.ui.components.animation :as animation] [status-im.ui.components.animation :as animation]
[status-im.ui.components.chat-icon.screen :as chat-icon]
[status-im.ui.components.fast-image :as fast-image] [status-im.ui.components.fast-image :as fast-image]
[status-im.ui.components.icons.icons :as icons] [status-im.ui.components.icons.icons :as icons]
[status-im.ui.components.list.views :as list]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.screens.chat.bottom-sheets.context-drawer :as message-context-drawer] [status-im.ui.screens.chat.bottom-sheets.context-drawer :as message-context-drawer]
[status-im.ui2.screens.chat.components.reply :as components.reply]
[status-im.ui.screens.chat.image.preview.views :as preview] [status-im.ui.screens.chat.image.preview.views :as preview]
[status-im.ui.screens.chat.message.audio :as message.audio] [status-im.ui.screens.chat.message.audio :as message.audio]
[status-im.ui.screens.chat.message.command :as message.command] [status-im.ui.screens.chat.message.command :as message.command]
@ -26,17 +31,13 @@
[status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.chat.sheets :as sheets] [status-im.ui.screens.chat.sheets :as sheets]
[status-im.ui.screens.chat.styles.message.message :as style] [status-im.ui.screens.chat.styles.message.message :as style]
[status-im.ui.screens.chat.utils :as chat.utils]
[status-im.ui.screens.chat.styles.photos :as photos.style] [status-im.ui.screens.chat.styles.photos :as photos.style]
[status-im.ui.screens.chat.utils :as chat.utils]
[status-im.ui.screens.communities.icon :as communities.icon] [status-im.ui.screens.communities.icon :as communities.icon]
[status-im.utils.handlers :refer [<sub >evt]] [status-im.ui2.screens.chat.components.reply :as components.reply]
[status-im.utils.config :as config] [status-im.utils.config :as config]
[status-im.utils.security :as security] [status-im.utils.handlers :refer [<sub >evt]]
[quo2.foundations.typography :as typography] [status-im.utils.security :as security])
[quo2.foundations.colors :as quo2.colors]
[status-im.ui.components.list.views :as list]
[quo.react-native :as rn]
[status-im.ui.components.chat-icon.screen :as chat-icon])
(:require-macros [status-im.utils.views :refer [defview letsubs]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defn message-timestamp-anim (defn message-timestamp-anim
@ -263,7 +264,7 @@
(defview community-content [{:keys [community-id] :as message}] (defview community-content [{:keys [community-id] :as message}]
(letsubs [{:keys [name description verified] :as community} [:communities/community community-id] (letsubs [{:keys [name description verified] :as community} [:communities/community community-id]
communities-enabled? [:communities/enabled?]] communities-enabled? [:communities/enabled?]]
(when (and communities-enabled? community) (when (and communities-enabled? community)
[rn/view {:style (assoc (style/message-wrapper message) [rn/view {:style (assoc (style/message-wrapper message)
:margin-vertical 10 :margin-vertical 10
@ -294,51 +295,51 @@
(defn message-content-wrapper (defn message-content-wrapper
"Author, userpic and delivery wrapper" "Author, userpic and delivery wrapper"
[{:keys [last-in-group? [{:keys [last-in-group? identicon from in-popover? timestamp-str
identicon deleted-for-me? deleted-for-me-undoable-till pinned]
from in-popover? timestamp-str
deleted-for-me? pinned]
:as message} content {:keys [modal close-modal]}] :as message} content {:keys [modal close-modal]}]
(let [response-to (:response-to (:content message))] (let [response-to (:response-to (:content message))]
[rn/view {:style (style/message-wrapper message) (if deleted-for-me?
:pointer-events :box-none [system-message/system-message
:accessibility-label :chat-item} {:type :deleted
(when (and (seq response-to) (:quoted-message message)) :label :message-deleted-for-you
[quoted-message response-to (:quoted-message message)]) :timestamp-str timestamp-str
[rn/view {:style (style/message-body) :non-pressable? true
:pointer-events :box-none} :animate-landing? (if deleted-for-me-undoable-till true false)}]
[rn/view (style/message-author-userpic) [rn/view {:style (style/message-wrapper message)
(when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) :pointer-events :box-none
[rn/touchable-highlight {:on-press #(do (when modal (close-modal)) :accessibility-label :chat-item}
(re-frame/dispatch [:chat.ui/show-profile from]))} (when (and (seq response-to) (:quoted-message message))
[photos/member-photo from identicon]])] [quoted-message response-to (:quoted-message message)])
[rn/view {:style (style/message-body)
:pointer-events :box-none}
[rn/view (style/message-author-userpic)
(when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned)
[rn/touchable-highlight {:on-press #(do (when modal (close-modal))
(re-frame/dispatch [:chat.ui/show-profile from]))}
[photos/member-photo from identicon]])]
[rn/view {:style (style/message-author-wrapper)} [rn/view {:style (style/message-author-wrapper)}
(when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned) (when (or (and (seq response-to) (:quoted-message message)) last-in-group? pinned)
[rn/view {:style {:flex-direction :row :align-items :center}} [rn/view {:style {:flex-direction :row :align-items :center}}
[rn/touchable-opacity {:style style/message-author-touchable [rn/touchable-opacity {:style style/message-author-touchable
:disabled in-popover? :disabled in-popover?
:on-press #(do (when modal (close-modal)) :on-press #(do (when modal (close-modal))
(re-frame/dispatch [:chat.ui/show-profile from]))} (re-frame/dispatch [:chat.ui/show-profile from]))}
[message-author-name from {:modal modal}]] [message-author-name from {:modal modal}]]
[rn/text [rn/text
{:style (merge {:style (merge
{:padding-left 5 {:padding-left 5
:margin-top 2} :margin-top 2}
(style/message-timestamp-text)) (style/message-timestamp-text))
:accessibility-label :message-timestamp} :accessibility-label :message-timestamp}
timestamp-str]]) timestamp-str]])
;; MESSAGE CONTENT ;; MESSAGE CONTENT
;; TODO(yqrashawn): wait for system message component to display deleted for me UI content
(if deleted-for-me? [link-preview/link-preview-wrapper (:links (:content message)) false false]]]
[rn/view {:style {:border-width 2 ;; delivery status
:border-color :red}} [rn/view (style/delivery-status)
content] [message-delivery-status message]]])))
content)
[link-preview/link-preview-wrapper (:links (:content message)) false false]]]
; delivery status
[rn/view (style/delivery-status)
[message-delivery-status message]]]))
(def image-max-width 260) (def image-max-width 260)
(def image-max-height 192) (def image-max-height 192)