Merge pull request #146 from status-im/feature/#119
Feature/#119
Former-commit-id: db5c654686
After Width: | Height: | Size: 647 B |
After Width: | Height: | Size: 604 B |
After Width: | Height: | Size: 358 B |
After Width: | Height: | Size: 501 B |
After Width: | Height: | Size: 436 B |
After Width: | Height: | Size: 227 B |
After Width: | Height: | Size: 930 B |
After Width: | Height: | Size: 836 B |
After Width: | Height: | Size: 361 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 578 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 787 B |
|
@ -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"]
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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})
|
|
@ -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)))
|
|
@ -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"})
|
||||
|
|
|
@ -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]))
|
|
@ -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)]]]))
|
||||
|
|
|
@ -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})
|
|
@ -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)]]]])
|
|
@ -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"
|
||||
|
|