Sync indicator (#312)

This commit is contained in:
Alexander Pantyuhov 2016-10-06 12:52:58 +03:00
parent 1128e5d089
commit 52b786dd25
15 changed files with 434 additions and 90 deletions

View File

@ -25,12 +25,12 @@
[status-im.chat.views.new-message :refer [chat-message-new]] [status-im.chat.views.new-message :refer [chat-message-new]]
[status-im.chat.views.actions :refer [actions-view]] [status-im.chat.views.actions :refer [actions-view]]
[status-im.chat.views.bottom-info :refer [bottom-info-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.i18n :refer [label label-pluralize]]
[status-im.components.animation :as anim] [status-im.components.animation :as anim]
[status-im.constants :refer [console-chat-id [status-im.components.sync-state.offline :refer [offline-view]]
content-type-status]] [status-im.constants :refer [content-type-status]]
[reagent.core :as r] [reagent.core :as r]
[clojure.string :as str]
[cljs-time.core :as t])) [cljs-time.core :as t]))
(defn contacts-by-identity [contacts] (defn contacts-by-identity [contacts]
@ -82,44 +82,6 @@
(assoc :last-message (= (js/parseInt index) (dec messages-count))))] (assoc :last-message (= (js/parseInt index) (dec messages-count))))]
(list-item [chat-message message]))) (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 [] (defn toolbar-action []
(let [show-actions (subscribe [:chat-ui-props :show-actions?])] (let [show-actions (subscribe [:chat-ui-props :show-actions?])]
(fn [] (fn []
@ -138,7 +100,7 @@
[view [view
[status-bar] [status-bar]
[toolbar {:hide-nav? show-actions [toolbar {:hide-nav? show-actions
:custom-content [toolbar-content] :custom-content [toolbar-content-view]
:custom-action [toolbar-action] :custom-action [toolbar-action]
:style (get-in platform-specific [:component-styles :toolbar])}]]) :style (get-in platform-specific [:component-styles :toolbar])}]])
@ -222,4 +184,5 @@
(when @show-actions? (when @show-actions?
[actions-view]) [actions-view])
(when @show-bottom-info? (when @show-bottom-info?
[bottom-info-view])])}))) [bottom-info-view])
[offline-view {:top (get-in platform-specific [:component-styles :status-bar :default :height])}]])})))

View File

@ -10,11 +10,14 @@
[status-im.components.toolbar.styles :refer [toolbar-background1]])) [status-im.components.toolbar.styles :refer [toolbar-background1]]))
(def chat-view (def chat-view
{:flex 1 {:flex 1
:backgroundColor chat-background}) :background-color chat-background})
(def toolbar-container
{})
(defn messages-container [bottom] (defn messages-container [bottom]
{:flex 1 {:flex 1
:padding-bottom bottom}) :padding-bottom bottom})
(def toolbar-view (def toolbar-view
@ -26,6 +29,7 @@
(def action (def action
{:width 56 {:width 56
:height 56 :height 56
:margin-top -2
:alignItems :center :alignItems :center
:justifyContent :center}) :justifyContent :center})
@ -40,14 +44,13 @@
:height 14}) :height 14})
(defn chat-name-view [show-actions] (defn chat-name-view [show-actions]
{:flex 1 {:flex 1
:marginLeft (if show-actions 16 0) :margin-left (if show-actions 16 0)
:alignItems :flex-start :align-items :flex-start
:justifyContent :center}) :justify-content :center})
(def chat-name-text (def chat-name-text
{:marginTop -2.5 {:color text1-color
:color text1-color
:fontSize 16}) :fontSize 16})
(def group-icon (def group-icon
@ -66,9 +69,13 @@
:color text2-color}) :color text2-color})
(def last-activity (def last-activity
{:marginTop 1 {:margin-top 3
:color text2-color :height 18})
:fontSize 12})
(def last-activity-text
{:color text2-color
:background-color :transparent
:font-size 12})
(defn actions-wrapper [status-bar-height] (defn actions-wrapper [status-bar-height]
{:backgroundColor toolbar-background1 {:backgroundColor toolbar-background1

View File

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

View File

@ -24,6 +24,7 @@
[status-im.chats-list.styles :as st] [status-im.chats-list.styles :as st]
[status-im.utils.platform :refer [platform-specific]] [status-im.utils.platform :refer [platform-specific]]
[status-im.components.tabs.bottom-gradient :refer [bottom-gradient]] [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]])) [status-im.components.tabs.styles :refer [tabs-height]]))
(defview chats-list-toolbar [] (defview chats-list-toolbar []
@ -86,4 +87,5 @@
:style st/list-container}] :style st/list-container}]
(when (get-in platform-specific [:chats :action-button?]) (when (get-in platform-specific [:chats :action-button?])
[chats-action-button]) [chats-action-button])
[bottom-gradient]]) [bottom-gradient]
[offline-view]])

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,14 @@
(def toolbar-gradient (def toolbar-gradient
{:height 4}) {:height 4})
(defn toolbar [background-color] (defn toolbar-wrapper [background-color]
{:flexDirection :row {:backgroundColor (or background-color toolbar-background1)
:backgroundColor (or background-color toolbar-background1)
:height toolbar-height
:elevation 2}) :elevation 2})
(def toolbar
{:flex-direction :row
:height toolbar-height})
(defn toolbar-nav-actions-container [actions] (defn toolbar-nav-actions-container [actions]
{:width (if (and actions (> (count actions) 0)) {:width (if (and actions (> (count actions) 0))
(-> (+ toolbar-icon-width toolbar-icon-spacing) (-> (+ toolbar-icon-width toolbar-icon-spacing)

View File

@ -6,6 +6,7 @@
text text
image image
touchable-highlight]] touchable-highlight]]
[status-im.components.sync-state.gradient :refer [sync-state-gradient-view]]
[status-im.components.styles :refer [icon-back]] [status-im.components.styles :refer [icon-back]]
[status-im.components.toolbar.styles :as st])) [status-im.components.toolbar.styles :as st]))
@ -17,30 +18,32 @@
background-color :background-color background-color :background-color
custom-content :custom-content custom-content :custom-content
style :style}] style :style}]
(let [style (merge (st/toolbar background-color) style)] (let [style (merge (st/toolbar-wrapper background-color) style)]
[view {:style style} [view {:style style}
[view (st/toolbar-nav-actions-container actions) [view st/toolbar
(when (not hide-nav?) [view (st/toolbar-nav-actions-container actions)
(if nav-action (when (not hide-nav?)
[touchable-highlight {:on-press (:handler nav-action)} (if nav-action
[view st/toolbar-nav-action [touchable-highlight {:on-press (:handler nav-action)}
[image (:image nav-action)]]] [view st/toolbar-nav-action
[touchable-highlight {:on-press #(dispatch [:navigate-back]) [image (:image nav-action)]]]
:accessibility-label :navigate-back} [touchable-highlight {:on-press #(dispatch [:navigate-back])
[view st/toolbar-nav-action :accessibility-label :navigate-back}
[image {:source {:uri :icon_back} [view st/toolbar-nav-action
:style icon-back}]]]))] [image {:source {:uri :icon_back}
(or custom-content :style icon-back}]]]))]
[view {:style st/toolbar-title-container} (or custom-content
[text {:style st/toolbar-title-text} [view {:style st/toolbar-title-container}
title]]) [text {:style st/toolbar-title-text}
[view st/toolbar-actions-container title]])
(if actions [view st/toolbar-actions-container
(for [{action-image :image (if actions
action-handler :handler} actions] (for [{action-image :image
^{:key (str "action-" action-image)} action-handler :handler} actions]
[touchable-highlight {:on-press action-handler} ^{:key (str "action-" action-image)}
[view st/toolbar-action [touchable-highlight {:on-press action-handler}
[image action-image]]]) [view st/toolbar-action
custom-action)]])) [image action-image]]])
custom-action)]]
[sync-state-gradient-view]]))

View File

@ -17,6 +17,7 @@
(def response-suggesstion-resize-duration 100) (def response-suggesstion-resize-duration 100)
(def default-number-of-messages 20) (def default-number-of-messages 20)
(def blocks-per-hour 120)
(def default-number-of-discovery-search-results 20) (def default-number-of-discovery-search-results 20)

View File

@ -47,7 +47,10 @@
:keyboard-height 0 :keyboard-height 0
:animations {;; todo clear this :animations {;; todo clear this
:tabs-bar-value (anim/create-value 0)} :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] (defn chat-staged-commands-path [chat-id]
[:chats chat-id :staged-commands]) [:chats chat-id :staged-commands])

View File

@ -57,6 +57,7 @@
(fn [_ [_ address]] (fn [_ [_ address]]
(dispatch [:initialize-account-db]) (dispatch [:initialize-account-db])
(dispatch [:initialize-protocol address]) (dispatch [:initialize-protocol address])
(dispatch [:initialize-sync-listener])
(dispatch [:initialize-chats]) (dispatch [:initialize-chats])
(dispatch [:load-contacts]) (dispatch [:load-contacts])
(dispatch [:init-chat]) (dispatch [:init-chat])

View File

@ -7,7 +7,8 @@
[status-im.data-store.pending-messages :as pending-messages] [status-im.data-store.pending-messages :as pending-messages]
[status-im.data-store.chats :as chats] [status-im.data-store.chats :as chats]
[status-im.protocol.core :as protocol] [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.i18n :refer [label]]
[status-im.utils.random :as random] [status-im.utils.random :as random]
[taoensso.timbre :as log :refer-macros [debug]])) [taoensso.timbre :as log :refer-macros [debug]]))
@ -43,6 +44,32 @@
(contacts/get-all))})] (contacts/get-all))})]
(assoc db :web3 w3))))) (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 (register-handler :incoming-message
(u/side-effect! (u/side-effect!
(fn [_ [_ type {:keys [payload] :as message}]] (fn [_ [_ type {:keys [payload] :as message}]]

View File

@ -7,6 +7,7 @@
:not-implemented "!not implemented" :not-implemented "!not implemented"
:chat-name "Chat name" :chat-name "Chat name"
:notifications-title "Notifications and sounds" :notifications-title "Notifications and sounds"
:offline "Offline"
;drawer ;drawer
:invite-friends "Invite friends" :invite-friends "Invite friends"
@ -23,11 +24,15 @@
:members-active {:one "1 member, 1 active" :members-active {:one "1 member, 1 active"
:other "{{count}} members, {{count}} active" :other "{{count}} members, {{count}} active"
:zero "no members"} :zero "no members"}
:active-online "online" :active-online "Online"
:active-unknown "unknown" :active-unknown "Unknown"
:available "available" :available "Available"
:no-messages "No messages" :no-messages "No messages"
;sync
:sync-in-progress "Syncing..."
:sync-synced "In sync"
;messages ;messages
:status-sending "Sending" :status-sending "Sending"
:status-pending "Sending" :status-pending "Sending"