From 07dfc956581b2112e8c947c2df0ac3e51cae6d58 Mon Sep 17 00:00:00 2001 From: bitsikka Date: Wed, 17 Jul 2019 19:09:23 +0545 Subject: [PATCH] [8556] fix - Last message jumps up after chat is opened Signed-off-by: Andrey Shovkoplyas --- src/status_im/chat/models.cljs | 6 +- .../ui/components/connectivity/styles.cljs | 3 +- .../ui/components/connectivity/view.cljs | 137 +++++++++++------- .../ui/screens/chat/styles/main.cljs | 25 +--- src/status_im/ui/screens/chat/views.cljs | 113 ++++++--------- .../ui/screens/desktop/main/chat/views.cljs | 2 +- .../ui/screens/home/filter/views.cljs | 47 +++--- src/status_im/ui/screens/home/styles.cljs | 4 +- src/status_im/ui/screens/home/views.cljs | 73 +++++----- 9 files changed, 204 insertions(+), 206 deletions(-) diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index fcc9eb5870..64c90b431d 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -166,7 +166,8 @@ :on-error (fn [error] (log/error "can't remove a chat:" error))}]} - (navigation/navigate-to-cofx :home {})) + (when (not (= (:view-id db) :home)) + (navigation/navigate-to-cofx :home {}))) (fx/merge cofx (mailserver/remove-gaps chat-id) (mailserver/remove-range chat-id) @@ -176,7 +177,8 @@ ;; TODO: this is not accurate, if there's a pending contact ;; request it will not be sent anymore (transport.protocol/remove-chat chat-id) - (navigation/navigate-to-cofx :home {})))) + (when (not (= (:view-id db) :home)) + (navigation/navigate-to-cofx :home {}))))) (defn- unread-messages-number [chats] (apply + (map :unviewed-messages-count chats))) diff --git a/src/status_im/ui/components/connectivity/styles.cljs b/src/status_im/ui/components/connectivity/styles.cljs index 44a73adde0..66416ba98f 100644 --- a/src/status_im/ui/components/connectivity/styles.cljs +++ b/src/status_im/ui/components/connectivity/styles.cljs @@ -4,9 +4,10 @@ [status-im.utils.platform :as platform])) (defnstyle text-wrapper - [{:keys [window-width height background-color opacity]}] + [{:keys [window-width height background-color opacity transform]}] (cond-> {:flex-direction :row :justify-content :center + :transform [{:translateY transform}] :opacity opacity :background-color (or background-color colors/gray) :height height} diff --git a/src/status_im/ui/components/connectivity/view.cljs b/src/status_im/ui/components/connectivity/view.cljs index 64a7a48c4b..805489c231 100644 --- a/src/status_im/ui/components/connectivity/view.cljs +++ b/src/status_im/ui/components/connectivity/view.cljs @@ -9,6 +9,9 @@ [status-im.utils.utils :as utils] [status-im.utils.platform :as platform])) +(def connectivity-bar-height 35) +(def neg-connectivity-bar-height (- connectivity-bar-height)) + (defn easing [direction n] {:toValue n :easing ((if (= :in direction) @@ -52,6 +55,7 @@ [react/view {:style {:width parent-width :position :absolute :top -3 + :z-index 3 :height 3 :background-color colors/white}} [react/animated-view {:style (animated-bar-style blue-bar-left-margin @@ -62,40 +66,42 @@ colors/white) :left (* 0.15 parent-width))}]])) -(defonce show-connected? (reagent/atom true)) +(def to-hide? (reagent/atom false)) -(defn manage-visibility [connected? anim-opacity anim-height] +(defn manage-visibility [connected? anim-opacity anim-y] (if connected? - (do (animation/start - (animation/parallel - [(animation/timing anim-opacity - {:toValue 0 - :delay 800 - :duration 150 - :easing (.-ease (animation/easing)) - :useNativeDriver true}) - (animation/timing anim-height - {:toValue (if platform/desktop? 0 -35) - :delay 800 - :duration 150 - :easing (.-ease (animation/easing)) - :useNativeDriver true})])) - (utils/set-timeout - #(reset! show-connected? false) - 2000)) - (do (reset! show-connected? true) - (animation/start - (animation/parallel - [(animation/timing anim-opacity - {:toValue 1 - :duration 150 - :easing (.-ease (animation/easing)) - :useNativeDriver true}) - (animation/timing anim-height - {:toValue (if platform/desktop? 35 0) - :duration 150 - :easing (.-ease (animation/easing)) - :useNativeDriver true})]))))) + (when @to-hide? + (animation/start + (animation/parallel + [(animation/timing anim-opacity + {:toValue 0 + :delay 800 + :duration 150 + :easing (.-ease (animation/easing)) + :useNativeDriver true}) + (animation/timing anim-y + {:toValue (if platform/desktop? 0 neg-connectivity-bar-height) + :delay 800 + :duration 150 + :easing (.-ease (animation/easing)) + :useNativeDriver true})]) + ;; second param of start() - a callback that fires when animation stops + #(reset! to-hide? false))) + ;; else + (animation/start + (animation/parallel + [(animation/timing anim-opacity + {:toValue 1 + :duration 150 + :easing (.-ease (animation/easing)) + :useNativeDriver true}) + (animation/timing anim-y + {:toValue (if platform/desktop? connectivity-bar-height 0) + :duration 150 + :easing (.-ease (animation/easing)) + :useNativeDriver true})]) + ;; second param of start() - a callback that fires when animation stops + #(reset! to-hide? true)))) (defn connectivity-status [{:keys [connected?]} anim-translate-y] @@ -109,23 +115,22 @@ (manage-visibility (:connected? (reagent/props comp)) anim-opacity anim-translate-y)) :reagent-render - (fn [{:keys [view-id message on-press-fn - connected? connecting?] :as opts}] + (fn [{:keys [view-id message on-press-fn connected? connecting?] :as opts}] [react/animated-view {:style (styles/text-wrapper (assoc opts :height (if platform/desktop? anim-translate-y - 35) + connectivity-bar-height) :background-color (if connected? colors/green colors/gray) + ;;TODO how does this affect desktop? + :transform anim-translate-y :opacity anim-opacity :modal? (= view-id :chat-modal))) :accessibility-label :connection-status-text} (when connecting? - [react/activity-indicator {:animated true - :color colors/white - :margin-right 6}]) + [react/activity-indicator {:color colors/white :margin-right 6}]) (if (= message :mobile-network) [react/nested-text {:style styles/text :on-press on-press-fn} @@ -136,28 +141,60 @@ :on-press on-press-fn} (i18n/label message)])])}))) -(defn connectivity-animation-wrapper [style anim-value & content] - (vec (concat - (if platform/desktop? - [react/view {:style {:flex 1}}] - [react/animated-view - {:style - (merge {:flex 1 - :transform [{:translateY anim-value}]} - style)}]) - content))) - (defview connectivity-view [anim-translate-y] (letsubs [status-properties [:connectivity/status-properties] view-id [:view-id] window-width (reagent/atom 0)] + {:component-will-mount + (fn [] + (if (:connected? status-properties) + (animation/set-value anim-translate-y neg-connectivity-bar-height) + (animation/set-value anim-translate-y 0)))} (let [{:keys [loading-indicator?]} status-properties] - [react/view {:style {:align-items :stretch} + [react/view {:style {:align-items :stretch + :z-index 1} :on-layout #(reset! window-width (-> % .-nativeEvent .-layout .-width))} (when loading-indicator? [loading-indicator @window-width]) + ;; This view below exists only to hide the connectivity-status bar when "connected". + ;; Ideally connectivity-status bar would be hidden under "toolbar/toolbar", + ;; but that has to be transparent(enven though it sits above the bar) + ;; to show through the "loading-indicator" + ;; TODO consider making the height the same height as the "toolbar/toolbar" + [react/view {:position :absolute + :top neg-connectivity-bar-height + :width @window-width + :z-index 2 + :height connectivity-bar-height + :background-color colors/white}] [connectivity-status (merge status-properties {:view-id view-id :window-width @window-width}) anim-translate-y]]))) + +;; "push?" determines whether "content" gets pushed down when disconnected +;; like in :home view, or stays put like in :chat view +;; TODO determine-how-this-affects/fix desktop +(defn connectivity-animation-wrapper [style anim-value push? & content] + (vec (concat + (if platform/desktop? + [react/view {:style {:flex 1}}] + [react/animated-view + {:style (merge {:flex 1 + :margin-bottom neg-connectivity-bar-height} + ;; A translated view (connectivity-view in this case) + ;; prevents touch interaction to component below + ;; them. If we don't bring this view on the same level + ;; or above as the translated view, the top + ;; portion(same height as connectivity-view) of + ;; "content" (which now occupies translated view's + ;; natural[untranslated] position) becomes + ;; unresponsive to touch + (when-not @to-hide? + {:z-index 1}) + (if push? + {:transform [{:translateY anim-value}]} + {:transform [{:translateY neg-connectivity-bar-height}]}) + style)}]) + content))) diff --git a/src/status_im/ui/screens/chat/styles/main.cljs b/src/status_im/ui/screens/chat/styles/main.cljs index 368c9f76a4..db38ff34fc 100644 --- a/src/status_im/ui/screens/chat/styles/main.cljs +++ b/src/status_im/ui/screens/chat/styles/main.cljs @@ -2,8 +2,7 @@ (:require [status-im.ui.components.colors :as colors])) (def chat-view - {:flex 1 - :background-color colors/white}) + {:flex 1}) (def toolbar-container {:flex 1 @@ -177,12 +176,6 @@ (def add-contact-close-icon {:margin-right 12}) -(def message-view-preview - {:flex 1 - :align-items :center - :justify-content :center - :background-color :white}) - (defn message-view-animated [opacity] {:opacity opacity :flex 1 @@ -195,22 +188,6 @@ :padding-vertical 50 :margin-right 6}) -#_(defn intro-header-container - [height status no-messages] - (let [adjusted-height (if (< height 280) 324 height)] - (if (or no-messages (= status (or :loading :empty))) - {:flex 1 - :flex-direction :column - :justify-content :center - :align-items :center - :height adjusted-height - :padding-horizontal 32} - {:flex 1 - :flex-direction :column - :justify-content :center - :align-items :center - :padding-horizontal 32}))) - (defn intro-header-container [height status no-messages] (let [adjusted-height (if (< height 280) 324 height)] diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index 1893d7c8ed..4ef37f32de 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -49,27 +49,6 @@ (list-selection/show {:title chat-name :options (actions/actions group-chat? chat-id public?)})) -(defn chat-toolbar - [{:keys [chat-name group-chat chat-id contact]} public? modal?] - [react/view {:style {:z-index 100}} - [status-bar/status-bar (when modal? {:type :modal-white})] - [toolbar/toolbar - {:chat? true} - (if modal? - [toolbar/nav-button - (toolbar.actions/close toolbar.actions/default-handler)] - toolbar/nav-back-home) - [toolbar-content/toolbar-content-view] - (when-not modal? - [toolbar/actions - [{:icon :main-icons/more - :icon-opts {:color :black - :accessibility-label :chat-menu-button} - :handler #(on-options chat-id chat-name group-chat public?)}]])] - (when (and (not group-chat) - (not (contact.db/added? contact))) - [add-contact-bar chat-id])]) - (defmulti message-row (fn [{{:keys [type]} :row}] type)) @@ -90,7 +69,7 @@ (def animation-duration 200) -(defview messages-view-animation [message-view] +(defview messages-view-animation [add-contact-bar message-view] ;; smooths out appearance of message-view (letsubs [opacity (animation/create-value 0)] {:component-did-mount (fn [_] @@ -100,19 +79,11 @@ {:toValue 1 :duration animation-duration :useNativeDriver true})))} - [react/with-activity-indicator - {:style style/message-view-preview - :preview [react/view style/message-view-preview]} - [react/touchable-without-feedback - {:on-press (fn [_] - (re-frame/dispatch [:chat.ui/set-chat-ui-props {:messages-focused? true - :show-stickers? false}]) - (when-not platform/desktop? - (react/dismiss-keyboard!)))} - (if platform/desktop? - message-view - [react/animated-view {:style (style/message-view-animated opacity)} - message-view])]])) + (if platform/desktop? + message-view + [react/animated-view {:style (style/message-view-animated opacity)} + add-contact-bar + message-view]))) (defn tribute-to-talk-header [name] @@ -354,8 +325,7 @@ :input-focused? false}]))} (let [no-messages (empty? messages) flat-list-conf - {:style {:margin-bottom -35} - :data messages + {:data messages :ref #(reset! messages-list-ref %) :footer [chat-intro-header-container chat no-messages] :key-fn #(or (:message-id %) (:value %)) @@ -427,39 +397,52 @@ :list-ref messages-list-ref}]))]]]))) (defview chat-root [modal?] - (letsubs [{:keys [public? chat-id show-input?] :as current-chat} + (letsubs [{:keys [public? chat-id chat-name show-input? group-chat contact] :as current-chat} [:chats/current-chat] current-chat-id [:chats/current-chat-id] show-message-options? [:chats/current-chat-ui-prop :show-message-options?] show-stickers? [:chats/current-chat-ui-prop :show-stickers?] two-pane-ui-enabled? [:two-pane-ui-enabled?] - anim-translate-y (animation/create-value (if two-pane-ui-enabled? 0 -35))] - ;; this check of current-chat-id is necessary only because in a fresh public chat creation sometimes - ;; this component renders before current-chat-id is set to current chat-id. Hence further down in sub - ;; components (e.g. chat-toolbar) there can be a brief visual inconsistancy like showing 'add contact' - ;; in public chat - (when (= chat-id current-chat-id) - [react/view {:style style/chat-view - :on-layout (fn [e] - (re-frame/dispatch [:set :layout-height (-> e .-nativeEvent .-layout .-height)]))} - ^{:key current-chat-id} - [chat-toolbar current-chat public? modal?] - (when-not two-pane-ui-enabled? - [connectivity/connectivity-view anim-translate-y]) - [connectivity/connectivity-animation-wrapper - {} - anim-translate-y - [messages-view-animation - ;;TODO(kozieiev) : When FlatList in react-native-desktop become viable it should be used instead of optimized ScrollView for chat - (if platform/desktop? - [messages-view-desktop current-chat modal?] - [messages-view current-chat modal?])]] - (when show-input? - [input/container]) - (when show-stickers? - [stickers/stickers-view]) - (when show-message-options? - [message-options/view])]))) + anim-translate-y (animation/create-value + (if two-pane-ui-enabled? 0 connectivity/neg-connectivity-bar-height))] + [react/view {:style style/chat-view + :on-layout (fn [e] + (re-frame/dispatch [:set :layout-height (-> e .-nativeEvent .-layout .-height)]))} + ^{:key current-chat-id} + [status-bar/status-bar (when modal? {:type :modal-white})] + [toolbar/toolbar + {:chat? true + :style {:z-index 2}} + (if modal? + [toolbar/nav-button + (toolbar.actions/close toolbar.actions/default-handler)] + toolbar/nav-back-home) + [toolbar-content/toolbar-content-view] + (when-not modal? + [toolbar/actions + [{:icon :main-icons/more + :icon-opts {:color :black + :accessibility-label :chat-menu-button} + :handler #(on-options chat-id chat-name group-chat public?)}]])] + (when-not two-pane-ui-enabled? + [connectivity/connectivity-view anim-translate-y]) + [connectivity/connectivity-animation-wrapper + {} + anim-translate-y + false + [messages-view-animation + (if (and (= chat-id current-chat-id) (not group-chat) (not (contact.db/added? contact))) + [add-contact-bar chat-id]) + ;;TODO(kozieiev) : When FlatList in react-native-desktop become viable it should be used instead of optimized ScrollView for chat + (if platform/desktop? + [messages-view-desktop current-chat modal?] + [messages-view current-chat modal?])]] + (when show-input? + [input/container]) + (when show-stickers? + [stickers/stickers-view]) + (when show-message-options? + [message-options/view])])) (defview chat [] [chat-root false]) diff --git a/src/status_im/ui/screens/desktop/main/chat/views.cljs b/src/status_im/ui/screens/desktop/main/chat/views.cljs index 9cd0f68b2f..4e45554c94 100644 --- a/src/status_im/ui/screens/desktop/main/chat/views.cljs +++ b/src/status_im/ui/screens/desktop/main/chat/views.cljs @@ -254,7 +254,7 @@ [message (:text content) (= from current-public-key) (assoc message-obj :group-chat group-chat :current-public-key current-public-key)]))]] - [connectivity/connectivity-view]]))) + [connectivity/connectivity-view nil]]))) (views/defview send-button [inp-ref disconnected?] (views/letsubs [{:keys [input-text]} [:chats/current-chat]] diff --git a/src/status_im/ui/screens/home/filter/views.cljs b/src/status_im/ui/screens/home/filter/views.cljs index a5898b390d..e6152c8bf9 100644 --- a/src/status_im/ui/screens/home/filter/views.cljs +++ b/src/status_im/ui/screens/home/filter/views.cljs @@ -51,22 +51,31 @@ (defonce search-input-state (reagent/atom {:show? false :height (animation/create-value - (- styles/search-input-height))})) + (- styles/search-input-height)) + :to-hide? false})) (defn show-search! [] - (swap! search-input-state assoc :show? true) - (animation/start - (animation/timing (:height @search-input-state) - {:toValue 0 - :duration 350 - :easing (.out (animation/easing) - (.-quad (animation/easing))) - :useNativeDriver true}))) + (when-not (:to-hide? @search-input-state) + (swap! search-input-state assoc :show? true) + (animation/start + (animation/timing (:height @search-input-state) + {:toValue 0 + :duration 350 + :easing (.out (animation/easing) + (.-quad (animation/easing))) + :useNativeDriver true}) + #(swap! search-input-state assoc :to-hide? true)))) + +(defn set-search-state-visible! + [visible?] + (swap! search-input-state assoc :show? visible?) + (swap! search-input-state assoc :to-hide? visible?) + (animation/set-value (:height @search-input-state) + (if visible? 0 (- styles/search-input-height)))) (defn reset-height [] - (animation/set-value (:height @search-input-state) - (- styles/search-input-height))) + (set-search-state-visible! false)) (defn hide-search! [] @@ -79,24 +88,16 @@ :duration 350 :easing (.in (animation/easing) (.-quad (animation/easing))) - :useNativeDriver true}))) - -(defn set-search-state-visible! - [visible?] - (swap! search-input-state assoc :show? visible?) - (animation/set-value (:height @search-input-state) - (if visible? - styles/search-input-height - 0))) + :useNativeDriver true}) + #(swap! search-input-state assoc :to-hide? false))) (defn search-input-wrapper [search-filter] (reagent/create-class {:component-will-unmount - #(set-search-state-visible! false) + #(set-search-state-visible! (:to-hide? @search-input-state)) :component-did-mount - #(when search-filter - (set-search-state-visible! true)) + #(set-search-state-visible! (:to-hide? @search-input-state)) :reagent-render (fn [search-filter] [search-input search-filter diff --git a/src/status_im/ui/screens/home/styles.cljs b/src/status_im/ui/screens/home/styles.cljs index 9ccde9d5b8..ef53ffdb79 100644 --- a/src/status_im/ui/screens/home/styles.cljs +++ b/src/status_im/ui/screens/home/styles.cljs @@ -185,13 +185,15 @@ :margin-horizontal 32 :color colors/gray}) -(def action-button-container +(defn action-button-container [home-width] {:position :absolute + :z-index 2 :align-items :center :bottom (+ tabs.styles/tabs-diff (cond platform/ios? 16 platform/android? 0 platform/desktop? 6)) + :left (- (/ home-width 2) 20) :width 40 :height 40}) diff --git a/src/status_im/ui/screens/home/views.cljs b/src/status_im/ui/screens/home/views.cljs index 41b3d4189b..7bbc42aea3 100644 --- a/src/status_im/ui/screens/home/views.cljs +++ b/src/status_im/ui/screens/home/views.cljs @@ -53,6 +53,7 @@ :label (i18n/label :t/get-started)}]]]) (defn home-empty-view [] + (filter.views/reset-height) [react/view styles/no-chats [react/i18n-text {:style styles/no-chats-text :key :no-recent-chats}] [react/view {:align-items :center :margin-top 20} @@ -62,14 +63,13 @@ (defn home-items-view [_ _ _ search-input-state] (let [previous-touch (reagent/atom nil) scrolling-from-top? (reagent/atom true)] - (filter.views/reset-height) (fn [search-filter chats all-home-items] (if (not-empty search-filter) [filter.views/home-filtered-items-list chats] [react/animated-view (merge {:style {:flex 1 - :margin-bottom -35 :background-color :white + :margin-bottom (- styles/search-input-height) :transform [{:translateY (:height @search-input-state)}]}} (when @scrolling-from-top? {:on-start-should-set-responder-capture @@ -86,13 +86,13 @@ current-timestamp (.-timestamp (.-nativeEvent event)) [previous-position previous-timestamp] @previous-touch] (when (and previous-position + (not (:show? @search-input-state)) (> 100 (- current-timestamp previous-timestamp)) (< 10 (- current-position previous-position))) (filter.views/show-search!))) false)})) - [list/flat-list {:style {:margin-bottom (- styles/search-input-height)} - :data all-home-items + [list/flat-list {:data all-home-items :key-fn first :footer [react/view {:style {:height tabs.styles/tabs-diff @@ -105,13 +105,13 @@ :render-fn (fn [home-item] [inner-item/home-list-item home-item])}] - (when (:show? @search-input-state) + (when (:to-hide? @search-input-state) [react/view {:width 1 :height styles/search-input-height}])])))) -(views/defview home-action-button [] +(views/defview home-action-button [home-width] (views/letsubs [logging-in? [:multiaccounts/login]] - [react/view styles/action-button-container + [react/view (styles/action-button-container home-width) [react/touchable-highlight {:accessibility-label :new-chat-button :on-press (when-not logging-in? #(re-frame/dispatch [:bottom-sheet/show-sheet :add-new {}]))} [react/view styles/action-button @@ -122,7 +122,7 @@ (views/defview home [loading?] (views/letsubs - [anim-translate-y (animation/create-value -35) + [anim-translate-y (animation/create-value connectivity/neg-connectivity-bar-height) {:keys [search-filter chats all-home-items]} [:home-items] window-width [:dimensions/window-width] two-pane-ui-enabled? [:two-pane-ui-enabled?]] @@ -136,41 +136,36 @@ (when two-pane-ui-enabled? {:border-right-width 1 :border-right-color colors/gray-light})) [status-bar/status-bar {:type :main}] - [react/keyboard-avoiding-view {:style {:flex 1 - :align-items :center} + [react/keyboard-avoiding-view {:style {:flex 1} :on-layout (fn [e] (re-frame/dispatch [:set-once :content-layout-height (-> e .-nativeEvent .-layout .-height)]))} - [react/view {:style {:flex 1 - :align-self :stretch}} - [toolbar/toolbar nil nil [toolbar/content-title (i18n/label :t/chat)]] - [les-debug-info] - (cond loading? - [react/view {:style {:flex 1 - :justify-content :center - :align-items :center}} - [connectivity/connectivity-view anim-translate-y] - [connectivity/connectivity-animation-wrapper - {} - anim-translate-y - [react/activity-indicator {:flex 1 - :animating true}]]] :else - [react/view {:style {:flex 1}} - [connectivity/connectivity-view anim-translate-y] - [connectivity/connectivity-animation-wrapper - {} - anim-translate-y - [filter.views/search-input-wrapper search-filter] - (if (and (not search-filter) - (empty? all-home-items)) - [home-empty-view] - [home-items-view - search-filter - chats - all-home-items - filter.views/search-input-state])]])] - [home-action-button]]]))) + [toolbar/toolbar {:style {:z-index 2}} nil [toolbar/content-title (i18n/label :t/chat)]] + ;; toolbar, connectivity-view, cannectivity-animation-wrapper are expected + ;; to be next to each other as siblings for them to work effctively. + ;; les-debug-info being here could disrupt that. Assuming its purpose is + ;; debug only, commenting it out for now. + ;; [les-debug-info] + [connectivity/connectivity-view anim-translate-y] + [connectivity/connectivity-animation-wrapper + {} + anim-translate-y + true + (if loading? + [react/activity-indicator {:flex 1 + :animating true}] + [react/view {:flex 1} + [filter.views/search-input-wrapper search-filter] + (if (and (not search-filter) + (empty? all-home-items)) + [home-empty-view] + [home-items-view + search-filter + chats + all-home-items + filter.views/search-input-state])])] + [home-action-button home-width]]]))) (views/defview home-wrapper [] (views/letsubs [loading? [:chats/loading?]]