Merge pull request #146 from status-im/feature/#119

Feature/#119

Former-commit-id: db5c654686
This commit is contained in:
Jarrad 2016-07-05 14:38:04 +02:00 committed by GitHub
commit 4ac074a71d
27 changed files with 368 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@ -3,8 +3,8 @@
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
:dependencies [[org.clojure/clojure "1.9.0-alpha7"]
[org.clojure/clojurescript "1.9.76"]
[reagent "0.5.1" :exclusions [cljsjs/react]]
[re-frame "0.6.0"]
[prismatic/schema "1.0.4"]

View File

@ -17,6 +17,9 @@
(defn anim-sequence [animations]
(.sequence animated (clj->js animations)))
(defn parallel [animations]
(.parallel animated (clj->js animations)))
(defn anim-delay [duration]
(.delay animated duration))

View File

@ -65,6 +65,10 @@
{:width 23
:height 22})
(def icon-scan
{:width 18
:height 18})
(def icon-plus
{:width 18
:height 18})
@ -94,10 +98,8 @@
(def button-input-container
{:flex 1
:flexDirection :row
:height 50})
:flexDirection :row})
(def button-input
{:flex 1
:flexDirection :column
:height 50})
:flexDirection :column})

View File

@ -0,0 +1,40 @@
(ns status-im.components.text-field.styles)
(def text-field-container
{:position :relative
:height 72
:paddingTop 30
:paddingBottom 7})
(def text-input
{:fontSize 16
:height 34
:lineHeight 34
:paddingBottom 5
:textAlignVertical :top})
(defn label [top font-size color]
{:position :absolute
:top top
:left 0
:color color
:fontSize font-size
:backgroundColor :transparent})
(def label-float
{})
(defn underline-container [backgroundColor]
{:backgroundColor backgroundColor
:height 1
:alignItems :center})
(defn underline [backgroundColor width]
{:backgroundColor backgroundColor
:height 1
:width width})
(defn error-text [color]
{:color color
:fontSize 12})

View File

@ -0,0 +1,189 @@
(ns status-im.components.text-field.view
(:require [clojure.string :as s]
[re-frame.core :refer [subscribe dispatch dispatch-sync]]
[reagent.core :as r]
[status-im.components.react :refer [react
view
text
animated-text
animated-view
text-input
touchable-opacity]]
[status-im.components.text-field.styles :as st]
[status-im.i18n :refer [label]]
[status-im.components.animation :as anim]
[status-im.utils.logging :as log]))
(def config {:label-top 16
:label-bottom 37
:label-font-large 16
:label-font-small 12
:label-animation-duration 200})
(def default-props {:wrapperStyle {}
:inputStyle {}
:lineStyle {}
:labelColor "#838c93"
:lineColor "#0000001f"
:focusLineColor "#0000001f"
:errorColor "#d50000"
:onFocus #()
:onBlur #()
:onChangeText #()
:onChange #()})
(defn field-animation [{:keys [top to-top font-size to-font-size
line-width to-line-width]}]
(let [duration (:label-animation-duration config)
animation (anim/parallel [(anim/timing top {:toValue to-top
:duration duration})
(anim/timing font-size {:toValue to-font-size
:duration duration})
(anim/timing line-width {:toValue to-line-width
:duration duration})])]
(anim/start animation (fn [arg]
(when (.-finished arg)
(log/debug "Field animation finished"))))))
; Invoked once before the component is mounted. The return value will be used
; as the initial value of this.state.
(defn get-initial-state [component]
{:has-focus false
:float-label? false
:label-top 0
:label-font-size 0
:line-width (anim/create-value 0)
:max-line-width 100})
; Invoked once, both on the client and server, immediately before the initial
; rendering occurs. If you call setState within this method, render() will see
; the updated state and will be executed only once despite the state change.
(defn component-will-mount [component]
(let [{:keys [value] :as props} (r/props component)
data {:label-top (anim/create-value (if (s/blank? value)
(:label-bottom config)
(:label-top config)))
:label-font-size (anim/create-value (if (s/blank? value)
(:label-font-large config)
(:label-font-small config)))
:float-label? (if (s/blank? value) false true)}]
(log/debug "component-will-mount")
(r/set-state component data)))
; Invoked once, only on the client (not on the server), immediately after the
; initial rendering occurs. At this point in the lifecycle, you can access any
; refs to your children (e.g., to access the underlying DOM representation).
; The componentDidMount() method of child components is invoked before that of
; parent components.
(defn component-did-mount [component]
(let [props (r/props component)]
(log/debug "component-did-mount:")))
; Invoked when a component is receiving new props. This method is not called for
; the initial render. Use this as an opportunity to react to a prop transition
; before render() is called by updating the state using this.setState().
; The old props can be accessed via this.props. Calling this.setState() within
; this function will not trigger an additional render.
(defn component-will-receive-props [component new-props]
(log/debug "component-will-receive-props: new-props=" new-props))
; Invoked before rendering when new props or state are being received. This method
; is not called for the initial render or when forceUpdate is used. Use this as
; an opportunity to return false when you're certain that the transition to the
; new props and state will not require a component update.
; If shouldComponentUpdate returns false, then render() will be completely skipped
; until the next state change. In addition, componentWillUpdate and
; componentDidUpdate will not be called.
(defn should-component-update [component next-props next-state]
(log/debug "should-component-update: " next-props next-state)
true)
; Invoked immediately before rendering when new props or state are being received.
; This method is not called for the initial render. Use this as an opportunity
; to perform preparation before an update occurs.
(defn component-will-update [component next-props next-state]
(log/debug "component-will-update: " next-props next-state))
; Invoked immediately after the component's updates are flushed to the DOM.
; This method is not called for the initial render. Use this as an opportunity
; to operate on the DOM when the component has been updated.
(defn component-did-update [component prev-props prev-state]
(log/debug "component-did-update: " prev-props prev-state))
(defn on-focus [{:keys [component animation onFocus]}]
(do
(log/debug "input focused")
(r/set-state component {:has-focus true
:float-label? true})
(field-animation animation)
(when onFocus (onFocus))))
(defn on-blur [{:keys [component value animation onBlur]}]
(do
(log/debug "Input blurred")
(r/set-state component {:has-focus false
:float-label? (if (s/blank? value) false true)})
(when (s/blank? value)
(field-animation animation))
(when onBlur (onBlur))))
(defn get-width [event]
(.-width (.-layout (.-nativeEvent event))))
(defn reagent-render [data children]
(let [component (r/current-component)
{:keys [has-focus
float-label?
label-top
label-font-size
line-width
max-line-width] :as state} (r/state component)
{:keys [wrapperStyle inputStyle lineColor focusLineColor
labelColor errorColor error label value onFocus onBlur
onChangeText onChange] :as props} (merge default-props (r/props component))
lineColor (if error errorColor lineColor)
focusLineColor (if error errorColor focusLineColor)
labelColor (if (and error (not float-label?)) errorColor labelColor)
label (if error (str label " *") label)]
(log/debug "reagent-render: " data state)
[view (merge st/text-field-container wrapperStyle)
[animated-text {:style (st/label label-top label-font-size labelColor)} label]
[text-input {:style (merge st/text-input inputStyle)
:placeholder ""
:onFocus #(on-focus {:component component
:animation {:top label-top
:to-top (:label-top config)
:font-size label-font-size
:to-font-size (:label-font-small config)
:line-width line-width
:to-line-width max-line-width}
:onFocus onFocus})
:onBlur #(on-blur {:component component
:value value
:animation {:top label-top
:to-top (:label-bottom config)
:font-size label-font-size
:to-font-size (:label-font-large config)
:line-width line-width
:to-line-width 0}
:onBlur onBlur})
:onChangeText #(onChangeText %)
:onChange #(onChange %)} value]
[view {:style (st/underline-container lineColor)
:onLayout #(r/set-state component {:max-line-width (get-width %)})}
[animated-view {:style (st/underline focusLineColor line-width)}]]
[text {:style (st/error-text errorColor)} error]]))
(defn text-field [data children]
(let [component-data {:get-initial-state get-initial-state
:component-will-mount component-will-mount
:component-did-mount component-did-mount
:component-will-receive-props component-will-receive-props
:should-component-update should-component-update
:component-will-update component-will-update
:component-did-update component-did-update
:display-name "text-field"
:reagent-render reagent-render}]
(log/debug "Creating text-field component: " data)
(r/create-class component-data)))

View File

@ -157,7 +157,8 @@
(def contact-form-container
{:flex 1
:color :white})
:color :white
:backgroundColor :white})
(def gradient-background
{:position :absolute
@ -168,4 +169,14 @@
(def form-container
{:marginLeft 16
:margin-top 50})
:margin-top 16})
(def address-explication-container
{:flex 1
:margin-top 30
:paddingLeft 16
:paddingRight 16})
(def address-explication
{:textAlign :center
:color "#838c93de"})

View File

@ -0,0 +1,21 @@
(ns status-im.contacts.validations
(:require [cljs.spec :as s]
[status-im.persistence.realm :as realm]))
(defn unique-identity? [identity]
(println identity)
(not (realm/exists? :contacts :whisper-identity identity)))
(defn valid-length? [identity]
(= 64 (count identity)))
(s/def ::identity-length valid-length?)
(s/def ::unique-identity unique-identity?)
(s/def ::not-empty-string (s/and string? not-empty))
(s/def ::name ::not-empty-string)
(s/def ::whisper-identity (s/and ::not-empty-string
::unique-identity
::identity-length))
(s/def ::contact (s/keys :req-un [::name ::whisper-identity]
:opt-un [::phone ::photo-path ::address]))

View File

@ -1,12 +1,14 @@
(ns status-im.contacts.views.new-contact
(:require-macros [status-im.utils.views :refer [defview]])
(:require [re-frame.core :refer [subscribe dispatch dispatch-sync]]
[clojure.string :as str]
[status-im.components.react :refer [view
text
text-input
image
linear-gradient
touchable-highlight]]
[status-im.components.text-field.view :refer [text-field]]
[status-im.utils.identicon :refer [identicon]]
[status-im.components.toolbar :refer [toolbar]]
[status-im.components.styles :refer [color-purple
@ -19,58 +21,60 @@
toolbar-title-text
button-input-container
button-input
white-form-text-input]]
[status-im.qr-scanner.views.import-button :refer [import-button]]
form-text-input]]
[status-im.qr-scanner.views.scan-button :refer [scan-button]]
[status-im.i18n :refer [label]]
[cljs.spec :as s]
[status-im.contacts.validations :as v]
[status-im.contacts.styles :as st]))
(def toolbar-title
[view toolbar-title-container
[text {:style (merge toolbar-title-text {:color color-white})}
(label :t/new-contact)]])
[text {:style toolbar-title-text}
(label :t/add-new-contact)]])
(defview contact-name-input [name]
[]
[text-input
{:underlineColorAndroid color-white
:placeholderTextColor color-white
:style white-form-text-input
:autoFocus true
:placeholder (label :t/contact-name)
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}
name])
[text-field
{:error (if (str/blank? name) "" nil)
:errorColor "#7099e6"
:value name
:label (label :t/name)
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}])
(defview contact-whisper-id-input [whisper-identity]
[view button-input-container
[text-input
{:underlineColorAndroid color-white
:placeholderTextColor color-white
:style (merge white-form-text-input button-input)
:autoFocus true
:placeholder (label :t/whisper-identity)
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}
whisper-identity]
[import-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]])
[]
(let [error (if (str/blank? whisper-identity) "" nil)
error (if (s/valid? ::v/whisper-identity whisper-identity)
error
"Please enter a valid address or scan a QR code")]
[view button-input-container
[text-field
{:error error
:errorColor "#7099e6"
:value whisper-identity
:wrapperStyle (merge button-input)
:label (label :t/address)
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}]
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]]))
(defview new-contact []
[{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]]
[view st/contact-form-container
[linear-gradient {:colors ["rgba(182, 116, 241, 1)" "rgba(107, 147, 231, 1)" "rgba(43, 171, 238, 1)"]
:start [0, 0]
:end [0.5, 1]
:locations [0, 0.8, 1]
:style st/gradient-background}]
[toolbar {:background-color :transparent
:nav-action {:image {:source {:uri :icon_back_white}
:style icon-back}
:handler #(dispatch [:navigate-back])}
:custom-content toolbar-title
:action {:image {:source {:uri :icon_add}
:style icon-search}
:handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
[view st/form-container
[contact-whisper-id-input whisper-identity]
[contact-name-input name]]])
(let [valid-contact? (s/valid? ::v/contact new-contact)]
[view st/contact-form-container
[toolbar {:background-color :white
:nav-action {:image {:source {:uri :icon_back}
:style icon-back}
:handler #(dispatch [:navigate-back])}
:custom-content toolbar-title
:action {:image {:source {:uri (if valid-contact?
:icon_ok_blue
:icon_ok_disabled)}
:style icon-search}
:handler #(when valid-contact? (dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)]))}}]
[view st/form-container
[contact-name-input name]
[contact-whisper-id-input whisper-identity]]
[view st/address-explication-container
[text {:style st/address-explication} (label :t/address-explication)]]]))

View File

@ -74,3 +74,25 @@
:flexDirection :column
:color color-white
:margin-left 8})
(def scan-button
{:position :absolute
:bottom 0
:right 16
:flex 1
:height 50
:alignItems :center})
(def scan-button-content
{:flex 1
:flexDirection :row
:height 50
:alignItems :center
:alignSelf :center})
(def scan-text
{:flex 1
:flexDirection :column
:color "#7099e6"
:margin-left 8})

View File

@ -0,0 +1,23 @@
(ns status-im.qr-scanner.views.scan-button
(:require-macros [status-im.utils.views :refer [defview]])
(:require [re-frame.core :refer [subscribe dispatch dispatch-sync]]
[status-im.components.react :refer [view
text
image
touchable-highlight]]
[status-im.components.toolbar :refer [toolbar]]
[status-im.components.drawer.view :refer [drawer-view open-drawer]]
[status-im.components.styles :refer [icon-scan]]
[status-im.i18n :refer [label]]
[status-im.qr-scanner.styles :as st]))
(defview scan-button [handler]
[]
[view st/scan-button
[touchable-highlight
{:on-press handler}
[view st/scan-button-content
[image {:source {:uri :scan_blue}
:style icon-scan}]
[text {:style st/scan-text} (label :t/scan-qr)]]]])

View File

@ -119,9 +119,12 @@
:You "You"
;new-contact
:add-new-contact "Add new contact"
:import-qr "Import"
:contact-name "Contact Name"
:scan-qr "Scan QR"
:name "Name"
:whisper-identity "Whisper Identity"
:address-explication "Maybe here should be some text explaining what an address is and where to look for it"
;login
:recover-from-passphrase "Recover from passphrase"