diff --git a/src/status_im/components/animation.cljs b/src/status_im/components/animation.cljs index a945d44f4c..20701b0d83 100644 --- a/src/status_im/components/animation.cljs +++ b/src/status_im/components/animation.cljs @@ -14,6 +14,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)) diff --git a/src/status_im/components/text_field/styles.cljs b/src/status_im/components/text_field/styles.cljs new file mode 100644 index 0000000000..582454b4d6 --- /dev/null +++ b/src/status_im/components/text_field/styles.cljs @@ -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}) diff --git a/src/status_im/components/text_field/view.cljs b/src/status_im/components/text_field/view.cljs new file mode 100644 index 0000000000..f3757ac1bc --- /dev/null +++ b/src/status_im/components/text_field/view.cljs @@ -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))) \ No newline at end of file diff --git a/src/status_im/contacts/styles.cljs b/src/status_im/contacts/styles.cljs index 7e9ed688c6..64cb693300 100644 --- a/src/status_im/contacts/styles.cljs +++ b/src/status_im/contacts/styles.cljs @@ -173,6 +173,7 @@ (def address-explication-container {:flex 1 + :margin-top 30 :paddingLeft 16 :paddingRight 16}) diff --git a/src/status_im/contacts/views/new_contact.cljs b/src/status_im/contacts/views/new_contact.cljs index ee0ae60929..c0cd517fb5 100644 --- a/src/status_im/contacts/views/new_contact.cljs +++ b/src/status_im/contacts/views/new_contact.cljs @@ -8,6 +8,7 @@ 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 @@ -28,7 +29,6 @@ [status-im.contacts.styles :as st])) - (def toolbar-title [view toolbar-title-container [text {:style toolbar-title-text} @@ -36,42 +36,43 @@ (defview contact-name-input [name] [] - [text-input - {:underlineColorAndroid "#0000001f" - :placeholderTextColor "#838c93de" - :style form-text-input - :autoFocus true - :placeholder (label :t/name) - :onChangeText #(dispatch [:set-in [:new-contact :name] %])} - name]) + [text-field + {:error (if (str/blank? name) "" nil) + :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 "#0000001f" - :placeholderTextColor "#838c93de" - :style (merge form-text-input button-input) - :autoFocus true - :placeholder (label :t/address) - :onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])} - whisper-identity] - [scan-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 + :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 - [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 (s/valid? ::v/contact new-contact) - :icon_ok_blue - :icon_ok_disabled)} - :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]] - [view st/address-explication-container - [text {:style st/address-explication} (label :t/address-explication)]]]) + (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)]]])) diff --git a/src/status_im/qr_scanner/styles.cljs b/src/status_im/qr_scanner/styles.cljs index 65838b9ec6..057bf186c8 100644 --- a/src/status_im/qr_scanner/styles.cljs +++ b/src/status_im/qr_scanner/styles.cljs @@ -78,7 +78,7 @@ (def scan-button {:position :absolute - :top 5 + :bottom 0 :right 16 :flex 1 :height 50