diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs index adec449948..34ea036f84 100644 --- a/src/status_im/chat/screen.cljs +++ b/src/status_im/chat/screen.cljs @@ -25,12 +25,12 @@ [status-im.chat.views.new-message :refer [chat-message-new]] [status-im.chat.views.actions :refer [actions-view]] [status-im.chat.views.bottom-info :refer [bottom-info-view]] + [status-im.chat.views.toolbar-content :refer [toolbar-content-view]] [status-im.i18n :refer [label label-pluralize]] [status-im.components.animation :as anim] - [status-im.constants :refer [console-chat-id - content-type-status]] + [status-im.components.sync-state.offline :refer [offline-view]] + [status-im.constants :refer [content-type-status]] [reagent.core :as r] - [clojure.string :as str] [cljs-time.core :as t])) (defn contacts-by-identity [contacts] @@ -82,44 +82,6 @@ (assoc :last-message (= (js/parseInt index) (dec messages-count))))] (list-item [chat-message message]))) -(defn online-text [contact chat-id] - (cond - (= chat-id console-chat-id) - (label :t/available) - - contact - (let [last-online (get contact :last-online) - last-online-date (time/to-date last-online) - now-date (t/now)] - (if (and (> last-online 0) - (<= last-online-date now-date)) - (time/time-ago last-online-date) - (label :t/active-unknown))) - - :else (label :t/active-unknown))) - -(defn toolbar-content [] - (let [{:keys [group-chat name contacts chat-id]} (subscribe [:chat-properties [:group-chat :name :contacts :chat-id]]) - show-actions (subscribe [:chat-ui-props :show-actions?]) - contact (subscribe [:get-in [:contacts @chat-id]])] - (fn [] - [view (st/chat-name-view @show-actions) - [text {:style st/chat-name-text - :number-of-lines 1} - (if (str/blank? @name) - (label :t/user-anonymous) - (or @name (label :t/chat-name)))] - (if @group-chat - [view {:flexDirection :row} - [icon :group st/group-icon] - [text {:style st/members - :font :medium} - (let [cnt (inc (count @contacts))] - (label-pluralize cnt :t/members-active))]] - [text {:style st/last-activity - :font :default} - (online-text @contact @chat-id)])]))) - (defn toolbar-action [] (let [show-actions (subscribe [:chat-ui-props :show-actions?])] (fn [] @@ -138,7 +100,7 @@ [view [status-bar] [toolbar {:hide-nav? show-actions - :custom-content [toolbar-content] + :custom-content [toolbar-content-view] :custom-action [toolbar-action] :style (get-in platform-specific [:component-styles :toolbar])}]]) @@ -222,4 +184,5 @@ (when @show-actions? [actions-view]) (when @show-bottom-info? - [bottom-info-view])])}))) + [bottom-info-view]) + [offline-view {:top (get-in platform-specific [:component-styles :status-bar :default :height])}]])}))) diff --git a/src/status_im/chat/styles/screen.cljs b/src/status_im/chat/styles/screen.cljs index c9fd35af25..a82ed76d30 100644 --- a/src/status_im/chat/styles/screen.cljs +++ b/src/status_im/chat/styles/screen.cljs @@ -10,11 +10,14 @@ [status-im.components.toolbar.styles :refer [toolbar-background1]])) (def chat-view - {:flex 1 - :backgroundColor chat-background}) + {:flex 1 + :background-color chat-background}) + +(def toolbar-container + {}) (defn messages-container [bottom] - {:flex 1 + {:flex 1 :padding-bottom bottom}) (def toolbar-view @@ -26,6 +29,7 @@ (def action {:width 56 :height 56 + :margin-top -2 :alignItems :center :justifyContent :center}) @@ -40,14 +44,13 @@ :height 14}) (defn chat-name-view [show-actions] - {:flex 1 - :marginLeft (if show-actions 16 0) - :alignItems :flex-start - :justifyContent :center}) + {:flex 1 + :margin-left (if show-actions 16 0) + :align-items :flex-start + :justify-content :center}) (def chat-name-text - {:marginTop -2.5 - :color text1-color + {:color text1-color :fontSize 16}) (def group-icon @@ -66,9 +69,13 @@ :color text2-color}) (def last-activity - {:marginTop 1 - :color text2-color - :fontSize 12}) + {:margin-top 3 + :height 18}) + +(def last-activity-text + {:color text2-color + :background-color :transparent + :font-size 12}) (defn actions-wrapper [status-bar-height] {:backgroundColor toolbar-background1 diff --git a/src/status_im/chat/views/toolbar_content.cljs b/src/status_im/chat/views/toolbar_content.cljs new file mode 100644 index 0000000000..4d5209c1a0 --- /dev/null +++ b/src/status_im/chat/views/toolbar_content.cljs @@ -0,0 +1,65 @@ +(ns status-im.chat.views.toolbar-content + (:require [re-frame.core :refer [subscribe dispatch]] + [clojure.string :as str] + [cljs-time.core :as t] + [status-im.components.react :refer [view + text + icon]] + [status-im.i18n :refer [label label-pluralize]] + [status-im.chat.styles.screen :as st] + [status-im.components.refreshable-text.view :refer [refreshable-text]] + [status-im.utils.datetime :as time] + [status-im.constants :refer [console-chat-id]])) + +(defn online-text [contact chat-id] + (cond + (= chat-id console-chat-id) (label :t/available) + contact (let [last-online (get contact :last-online) + last-online-date (time/to-date last-online) + now-date (t/now)] + (if (and (> last-online 0) + (<= last-online-date now-date)) + (time/time-ago last-online-date) + (label :t/active-unknown))) + :else (label :t/active-unknown))) + +(defn last-activity [{:keys [online-text sync-state]}] + [refreshable-text {:style st/last-activity + :text-style st/last-activity-text + :font :default + :value (case sync-state + :in-progress (label :t/sync-in-progress) + :synced (label :t/sync-synced) + online-text)}]) + +(defn group-last-activity [{:keys [contacts sync-state]}] + (if (or (= sync-state :in-progress) + (= sync-state :synced)) + [last-activity {:sync-state sync-state}] + [view {:flex-direction :row} + [icon :group st/group-icon] + [text {:style st/members + :font :medium} + (let [cnt (inc (count contacts))] + (label-pluralize cnt :t/members-active))]])) + +(defn toolbar-content-view [] + (let [{:keys [group-chat + name + contacts + chat-id]} (subscribe [:chat-properties [:group-chat :name :contacts :chat-id]]) + show-actions (subscribe [:chat-ui-props :show-actions?]) + contact (subscribe [:get-in [:contacts @chat-id]]) + sync-state (subscribe [:get :sync-state])] + (fn [] + [view (st/chat-name-view @show-actions) + [text {:style st/chat-name-text + :number-of-lines 1} + (if (str/blank? @name) + (label :t/user-anonymous) + (or @name (label :t/chat-name)))] + (if @group-chat + [group-last-activity {:contacts @contacts + :sync-state @sync-state}] + [last-activity {:online-text (online-text @contact @chat-id) + :sync-state @sync-state}])]))) \ No newline at end of file diff --git a/src/status_im/chats_list/screen.cljs b/src/status_im/chats_list/screen.cljs index 9683860905..e21b6bed47 100644 --- a/src/status_im/chats_list/screen.cljs +++ b/src/status_im/chats_list/screen.cljs @@ -24,6 +24,7 @@ [status-im.chats-list.styles :as st] [status-im.utils.platform :refer [platform-specific]] [status-im.components.tabs.bottom-gradient :refer [bottom-gradient]] + [status-im.components.sync-state.offline :refer [offline-view]] [status-im.components.tabs.styles :refer [tabs-height]])) (defview chats-list-toolbar [] @@ -86,4 +87,5 @@ :style st/list-container}] (when (get-in platform-specific [:chats :action-button?]) [chats-action-button]) - [bottom-gradient]]) + [bottom-gradient] + [offline-view]]) diff --git a/src/status_im/components/refreshable_text/view.cljs b/src/status_im/components/refreshable_text/view.cljs new file mode 100644 index 0000000000..817b8c0b88 --- /dev/null +++ b/src/status_im/components/refreshable_text/view.cljs @@ -0,0 +1,67 @@ +(ns status-im.components.refreshable-text.view + (:require [reagent.core :as r] + [reagent.impl.util :as ru] + [status-im.components.react :refer [view + animated-view + text]] + [status-im.components.animation :as anim])) + +(defn start-animation [{:keys [old-value-top + new-value-top + old-value-opacity + new-value-opacity]}] + (anim/start + (anim/timing old-value-top {:toValue 10 + :duration 300})) + (anim/start + (anim/timing new-value-top {:toValue 0 + :duration 300})) + (anim/start + (anim/timing old-value-opacity {:toValue 0 + :duration 300})) + (anim/start + (anim/timing new-value-opacity {:toValue 1.0 + :duration 300}))) + +(defn refreshable-text [{:keys [value]}] + (let [old-value-top (anim/create-value 0) + new-value-top (anim/create-value 0) + old-value-opacity (anim/create-value 0) + new-value-opacity (anim/create-value 1.0) + context {:old-value-top old-value-top + :new-value-top new-value-top + :old-value-opacity old-value-opacity + :new-value-opacity new-value-opacity}] + (r/create-class + {:get-initial-state + (fn [] + {:old-value nil + :value value}) + :component-will-update + (fn [component props] + (let [{new-value :value} (ru/extract-props props) + {old-value :value} (r/props component)] + (r/set-state component {:old-value old-value + :value new-value}) + (anim/set-value old-value-top 0) + (anim/set-value new-value-top -10) + (anim/set-value old-value-opacity 1.0) + (anim/set-value new-value-opacity 0.0) + (start-animation context))) + :reagent-render + (fn [{:keys [style text-style font]}] + (let [component (r/current-component) + {:keys [old-value value]} (r/state component)] + [view style + [animated-view {:style {:position :absolute + :margin-top old-value-top + :opacity old-value-opacity}} + [text {:style text-style + :font font} + old-value]] + [animated-view {:style {:position :absolute + :margin-top new-value-top + :opacity new-value-opacity}} + [text {:style text-style + :font font} + value]]]))}))) \ No newline at end of file diff --git a/src/status_im/components/sync_state/gradient.cljs b/src/status_im/components/sync_state/gradient.cljs new file mode 100644 index 0000000000..c05f90795b --- /dev/null +++ b/src/status_im/components/sync_state/gradient.cljs @@ -0,0 +1,119 @@ +(ns status-im.components.sync-state.gradient + (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] + [status-im.components.react :refer [view + text + animated-view + linear-gradient + get-dimensions]] + [status-im.components.sync-state.styles :as st] + [status-im.components.animation :as anim] + [taoensso.timbre :as log])) + +(def gradient-animation-duration 700) +(def synced-disappear-delay 2500) +(def gradient-width 250) +(def in-progress-animation-delay 1500) + +(def window-width (:width (get-dimensions "window"))) + +(declare start-gradient-reverse-animation) + +(defn- start-gradient-animation [{:keys [gradient-position sync-state] :as context}] + (when (= @sync-state :in-progress) + (anim/start + (anim/timing gradient-position + {:toValue (- window-width (/ gradient-width 3)) + :duration gradient-animation-duration}) + (fn [_] + (start-gradient-reverse-animation context))))) + +(defn- start-gradient-reverse-animation [{:keys [gradient-position sync-state] :as context}] + (when (= @sync-state :in-progress) + (anim/start + (anim/timing gradient-position + {:toValue (- 0 (* 2 (/ gradient-width 3))) + :duration gradient-animation-duration}) + (fn [_] + (start-gradient-animation context))))) + +(defn- start-synced-animation [{:keys [sync-state-opacity in-progress-opacity synced-opacity]}] + (anim/start + (anim/timing in-progress-opacity {:toValue 0.0 + :duration 250})) + (anim/start + (anim/timing synced-opacity {:toValue 1.0 + :duration 250}) + (fn [_] + (anim/start + (anim/timing sync-state-opacity {:toValue 0.0 + :duration 250 + :delay synced-disappear-delay}) + (fn [_] + (dispatch [:set :sync-state :done])))))) + +(defn start-in-progress-animation [component] + (r/set-state component + {:pending? true + :animation (js/setTimeout + (fn [] + (dispatch [:set :sync-state :in-progress]) + (r/set-state component {:pending? false})) + in-progress-animation-delay)})) + +(defn start-offline-animation [{:keys [sync-state-opacity]}] + (anim/start + (anim/timing sync-state-opacity {:toValue 0.0 + :duration 250}))) + +(defn clear-pending-animation [component] + (let [{:keys [pending? animation]} (r/state component)] + (when pending? + (r/set-state component {:pending? false}) + (js/clearTimeout animation)))) + + +(defn sync-state-gradient-view [] + (let [sync-state (subscribe [:get :sync-state]) + gradient-position (anim/create-value 0) + sync-state-opacity (anim/create-value 0.0) + in-progress-opacity (anim/create-value 0.0) + synced-opacity (anim/create-value 0.0) + + context {:sync-state sync-state + :gradient-position gradient-position + + :sync-state-opacity sync-state-opacity + :in-progress-opacity in-progress-opacity + :synced-opacity synced-opacity} + on-update (fn [component _] + (case @sync-state + :pending (start-in-progress-animation component) + :in-progress (do + (anim/set-value gradient-position 0) + (anim/set-value sync-state-opacity 1) + (anim/set-value in-progress-opacity 1) + (anim/set-value synced-opacity 0) + (start-gradient-animation context)) + :synced (start-synced-animation context) + :done (clear-pending-animation component) + :offline (do (clear-pending-animation component) + (start-offline-animation context)) + (log/debug "Sync state:" @sync-state)))] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [] + [view st/sync-style-gradient + [animated-view {:style (st/loading-wrapper sync-state-opacity)} + [animated-view {:style (st/gradient-wrapper in-progress-opacity gradient-position)} + [linear-gradient {:colors ["#89b1fe" "#8b5fe4" "#8b5fe4" "#89b1fe"] + :start [0, 1] + :end [1, 1] + :locations [0 0.3 0.7 1] + :style (st/gradient gradient-width)}]] + (when (not= @sync-state :in-progress) + [animated-view {:style (st/synced-wrapper synced-opacity window-width)}])]])}))) \ No newline at end of file diff --git a/src/status_im/components/sync_state/offline.cljs b/src/status_im/components/sync_state/offline.cljs new file mode 100644 index 0000000000..4464923353 --- /dev/null +++ b/src/status_im/components/sync_state/offline.cljs @@ -0,0 +1,38 @@ +(ns status-im.components.sync-state.offline + (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] + [status-im.components.react :refer [view + text + animated-view + linear-gradient + get-dimensions]] + [status-im.components.sync-state.styles :as st] + [status-im.components.animation :as anim] + [status-im.i18n :refer [label]])) + +(def window-width (:width (get-dimensions "window"))) + +(defn start-offline-animation [offline-opacity] + (anim/start + (anim/timing offline-opacity {:toValue 1.0 + :duration 250}))) + +(defn offline-view [_] + (let [sync-state (subscribe [:get :sync-state]) + offline-opacity (anim/create-value 0.0) + on-update (fn [_ _] + (anim/set-value offline-opacity 0) + (when (= @sync-state :offline) + (start-offline-animation offline-opacity)))] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [{:keys [top]}] + (when (= @sync-state :offline) + [animated-view {:style (st/offline-wrapper top offline-opacity window-width)} + [view + [text {:style st/offline-text} + (label :t/offline)]]]))}))) \ No newline at end of file diff --git a/src/status_im/components/sync_state/styles.cljs b/src/status_im/components/sync_state/styles.cljs new file mode 100644 index 0000000000..1411802d2d --- /dev/null +++ b/src/status_im/components/sync_state/styles.cljs @@ -0,0 +1,41 @@ +(ns status-im.components.sync-state.styles) + +(def sync-style-gradient + {:position :relative + :height 0 + :top -2}) + +(defn loading-wrapper [opacity] + {:background-color "#89b1fe" + :opacity opacity + :height 2}) + +(defn gradient-wrapper [in-progress-opacity position] + {:position :absolute + :left position + :opacity in-progress-opacity}) + +(defn gradient [width] + {:width width + :height 2}) + +(defn synced-wrapper [opacity window-width] + {:opacity opacity + :position :absolute + :width window-width + :background-color "#5fc48d" + :height 2}) + +(defn offline-wrapper [top opacity window-width] + {:opacity opacity + :width window-width + :top (+ 56 top) + :position :absolute + :background-color "#828b92cc" + :height 35}) + +(def offline-text + {:text-align :center + :color :white + :font-size 14 + :top 8}) \ No newline at end of file diff --git a/src/status_im/components/toolbar/styles.cljs b/src/status_im/components/toolbar/styles.cljs index 14e256d289..904055785e 100644 --- a/src/status_im/components/toolbar/styles.cljs +++ b/src/status_im/components/toolbar/styles.cljs @@ -13,12 +13,14 @@ (def toolbar-gradient {:height 4}) -(defn toolbar [background-color] - {:flexDirection :row - :backgroundColor (or background-color toolbar-background1) - :height toolbar-height +(defn toolbar-wrapper [background-color] + {:backgroundColor (or background-color toolbar-background1) :elevation 2}) +(def toolbar + {:flex-direction :row + :height toolbar-height}) + (defn toolbar-nav-actions-container [actions] {:width (if (and actions (> (count actions) 0)) (-> (+ toolbar-icon-width toolbar-icon-spacing) diff --git a/src/status_im/components/toolbar/view.cljs b/src/status_im/components/toolbar/view.cljs index e1f9d82af5..3d57200ddb 100644 --- a/src/status_im/components/toolbar/view.cljs +++ b/src/status_im/components/toolbar/view.cljs @@ -6,6 +6,7 @@ text image touchable-highlight]] + [status-im.components.sync-state.gradient :refer [sync-state-gradient-view]] [status-im.components.styles :refer [icon-back]] [status-im.components.toolbar.styles :as st])) @@ -17,30 +18,32 @@ background-color :background-color custom-content :custom-content style :style}] - (let [style (merge (st/toolbar background-color) style)] + (let [style (merge (st/toolbar-wrapper background-color) style)] [view {:style style} - [view (st/toolbar-nav-actions-container actions) - (when (not hide-nav?) - (if nav-action - [touchable-highlight {:on-press (:handler nav-action)} - [view st/toolbar-nav-action - [image (:image nav-action)]]] - [touchable-highlight {:on-press #(dispatch [:navigate-back]) - :accessibility-label :navigate-back} - [view st/toolbar-nav-action - [image {:source {:uri :icon_back} - :style icon-back}]]]))] - (or custom-content - [view {:style st/toolbar-title-container} - [text {:style st/toolbar-title-text} - title]]) - [view st/toolbar-actions-container - (if actions - (for [{action-image :image - action-handler :handler} actions] - ^{:key (str "action-" action-image)} - [touchable-highlight {:on-press action-handler} - [view st/toolbar-action - [image action-image]]]) - custom-action)]])) + [view st/toolbar + [view (st/toolbar-nav-actions-container actions) + (when (not hide-nav?) + (if nav-action + [touchable-highlight {:on-press (:handler nav-action)} + [view st/toolbar-nav-action + [image (:image nav-action)]]] + [touchable-highlight {:on-press #(dispatch [:navigate-back]) + :accessibility-label :navigate-back} + [view st/toolbar-nav-action + [image {:source {:uri :icon_back} + :style icon-back}]]]))] + (or custom-content + [view {:style st/toolbar-title-container} + [text {:style st/toolbar-title-text} + title]]) + [view st/toolbar-actions-container + (if actions + (for [{action-image :image + action-handler :handler} actions] + ^{:key (str "action-" action-image)} + [touchable-highlight {:on-press action-handler} + [view st/toolbar-action + [image action-image]]]) + custom-action)]] + [sync-state-gradient-view]])) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 65ed3a0b67..7f0b75a5fa 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -17,6 +17,7 @@ (def response-suggesstion-resize-duration 100) (def default-number-of-messages 20) +(def blocks-per-hour 120) (def default-number-of-discovery-search-results 20) diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index 60949ea6dd..4f6ac879fe 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -47,7 +47,10 @@ :keyboard-height 0 :animations {;; todo clear this :tabs-bar-value (anim/create-value 0)} - :loading-allowed true}) + :loading-allowed true + + :sync-state :done + :sync-listener nil}) (defn chat-staged-commands-path [chat-id] [:chats chat-id :staged-commands]) diff --git a/src/status_im/handlers.cljs b/src/status_im/handlers.cljs index 9c1cdd1682..5c77134318 100644 --- a/src/status_im/handlers.cljs +++ b/src/status_im/handlers.cljs @@ -57,6 +57,7 @@ (fn [_ [_ address]] (dispatch [:initialize-account-db]) (dispatch [:initialize-protocol address]) + (dispatch [:initialize-sync-listener]) (dispatch [:initialize-chats]) (dispatch [:load-contacts]) (dispatch [:init-chat]) diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index aa56b58a4c..0689bbd375 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -7,7 +7,8 @@ [status-im.data-store.pending-messages :as pending-messages] [status-im.data-store.chats :as chats] [status-im.protocol.core :as protocol] - [status-im.constants :refer [text-content-type]] + [status-im.constants :refer [text-content-type + blocks-per-hour]] [status-im.i18n :refer [label]] [status-im.utils.random :as random] [taoensso.timbre :as log :refer-macros [debug]])) @@ -43,6 +44,32 @@ (contacts/get-all))})] (assoc db :web3 w3))))) +(register-handler :update-sync-state + (u/side-effect! + (fn [{:keys [sync-state]} [_ error sync]] + (let [{:keys [highestBlock currentBlock]} (js->clj sync :keywordize-keys true) + syncing? (> (- highestBlock currentBlock) blocks-per-hour) + new-state (cond + error :offline + syncing? (if (= sync-state :done) + :pending + :in-progress) + :else (if (or (= sync-state :done) + (= sync-state :pending)) + :done + :synced))] + (when (not= sync-state new-state) + (dispatch [:set :sync-state new-state])))))) + +(register-handler :initialize-sync-listener + (fn [{:keys [web3 sync-listener] :as db} _] + (when sync-listener + (.stopWatching sync-listener)) + (->> (.isSyncing (.-eth web3) + (fn [error sync] + (dispatch [:update-sync-state error sync]))) + (assoc db :sync-listener)))) + (register-handler :incoming-message (u/side-effect! (fn [_ [_ type {:keys [payload] :as message}]] diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 9db362b5ed..025d56e39e 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -7,6 +7,7 @@ :not-implemented "!not implemented" :chat-name "Chat name" :notifications-title "Notifications and sounds" + :offline "Offline" ;drawer :invite-friends "Invite friends" @@ -23,11 +24,15 @@ :members-active {:one "1 member, 1 active" :other "{{count}} members, {{count}} active" :zero "no members"} - :active-online "online" - :active-unknown "unknown" - :available "available" + :active-online "Online" + :active-unknown "Unknown" + :available "Available" :no-messages "No messages" + ;sync + :sync-in-progress "Syncing..." + :sync-synced "In sync" + ;messages :status-sending "Sending" :status-pending "Sending"