diff --git a/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png new file mode 100644 index 0000000000..66e62ce1a3 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..dc6068da44 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-hdpi/scan_blue.png b/android/app/src/main/res/drawable-hdpi/scan_blue.png new file mode 100644 index 0000000000..7ea6117534 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png new file mode 100644 index 0000000000..8b6c5482cf Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..62a6bdebf4 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-mdpi/scan_blue.png b/android/app/src/main/res/drawable-mdpi/scan_blue.png new file mode 100644 index 0000000000..f154c2ee43 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..df4ddc7541 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..07c323e77e Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/scan_blue.png b/android/app/src/main/res/drawable-xhdpi/scan_blue.png new file mode 100644 index 0000000000..df46334a87 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..9e8661249d Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..513a2177a2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/scan_blue.png b/android/app/src/main/res/drawable-xxhdpi/scan_blue.png new file mode 100644 index 0000000000..b89f57307c Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..f8a9b938b4 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..5f4358614b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png b/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png new file mode 100644 index 0000000000..2365f04fc5 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png differ diff --git a/project.clj b/project.clj index 1f1b0c77d2..af80bbed96 100644 --- a/project.clj +++ b/project.clj @@ -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"] diff --git a/src/status_im/components/animation.cljs b/src/status_im/components/animation.cljs index 74d3cb3f78..0176b6e360 100644 --- a/src/status_im/components/animation.cljs +++ b/src/status_im/components/animation.cljs @@ -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)) diff --git a/src/status_im/components/styles.cljs b/src/status_im/components/styles.cljs index aaa82f5a42..22f6705300 100644 --- a/src/status_im/components/styles.cljs +++ b/src/status_im/components/styles.cljs @@ -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}) 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 4da8803d96..64cb693300 100644 --- a/src/status_im/contacts/styles.cljs +++ b/src/status_im/contacts/styles.cljs @@ -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"}) diff --git a/src/status_im/contacts/validations.cljs b/src/status_im/contacts/validations.cljs new file mode 100644 index 0000000000..ca3feee759 --- /dev/null +++ b/src/status_im/contacts/validations.cljs @@ -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])) diff --git a/src/status_im/contacts/views/new_contact.cljs b/src/status_im/contacts/views/new_contact.cljs index a45de304a6..2dbe6abc82 100644 --- a/src/status_im/contacts/views/new_contact.cljs +++ b/src/status_im/contacts/views/new_contact.cljs @@ -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)]]])) diff --git a/src/status_im/qr_scanner/styles.cljs b/src/status_im/qr_scanner/styles.cljs index 05198031b1..057bf186c8 100644 --- a/src/status_im/qr_scanner/styles.cljs +++ b/src/status_im/qr_scanner/styles.cljs @@ -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}) \ No newline at end of file diff --git a/src/status_im/qr_scanner/views/import-qr-button.cljs b/src/status_im/qr_scanner/views/import-button.cljs similarity index 100% rename from src/status_im/qr_scanner/views/import-qr-button.cljs rename to src/status_im/qr_scanner/views/import-button.cljs diff --git a/src/status_im/qr_scanner/views/scan-button.cljs b/src/status_im/qr_scanner/views/scan-button.cljs new file mode 100644 index 0000000000..9ed1fd39e5 --- /dev/null +++ b/src/status_im/qr_scanner/views/scan-button.cljs @@ -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)]]]]) \ No newline at end of file diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 792dc4ebd5..c667275426 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -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"