diff --git a/src/status_im/contexts/profile/edit/modal/events.cljs b/src/status_im/contexts/profile/edit/modal/events.cljs new file mode 100644 index 0000000000..dc0809e001 --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/events.cljs @@ -0,0 +1,51 @@ +(ns status-im.contexts.profile.edit.modal.events + (:require [status-im.contexts.profile.edit.introduce-yourself.view :as introduce-yourself] + [utils.re-frame :as rf])) + +(rf/reg-event-fx + :profile/edit-profile + (fn [_ [{:keys [display-name picture color on-success]}]] + {:fx (cond-> [] + display-name + (conj [:dispatch + [:profile/edit-name + {:display-name display-name + :navigate-back? false + :show-toast? false}]]) + color + (conj [:dispatch + [:profile/edit-accent-colour + {:color color + :navigate-back? false + :show-toast? false}]]) + picture + (conj [:dispatch + [:profile/edit-picture + {:picture picture + :show-toast? false}]]) + + (nil? picture) + (conj [:dispatch [:profile/delete-picture {:show-toast? false}]]) + + :always + (conj [:dispatch-n [[:navigate-back] on-success]]))})) + + +(rf/reg-event-fx + :profile/ask-profile-update + (fn [_ [pending-event]] + {:fx [[:effects.async-storage/set {:update-profile-asked? true}] + [:dispatch + [:show-bottom-sheet + {:content (fn [] + [introduce-yourself/sheet {:pending-event pending-event}])}]]]})) + +(rf/reg-event-fx + :profile/check-profile-update-prompt + (fn [_ [pending-event]] + {:fx [[:effects.async-storage/get + {:keys [:update-profile-asked?] + :cb (fn [{:keys [update-profile-asked?]}] + (rf/dispatch (if update-profile-asked? + pending-event + [:profile/ask-profile-update pending-event])))}]]})) diff --git a/src/status_im/contexts/profile/edit/modal/style.cljs b/src/status_im/contexts/profile/edit/modal/style.cljs new file mode 100644 index 0000000000..6bdf51d993 --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/style.cljs @@ -0,0 +1,60 @@ +(ns status-im.contexts.profile.edit.modal.style + (:require + [quo.foundations.colors :as colors] + [react-native.platform :as platform])) + +(def continue-button + {:width "100%" + :margin-left :auto + :margin-top (if platform/android? :auto 0) + :margin-right :auto}) + +(def button-container + {:width "100%" + :padding-left 20 + :padding-right 20 + :padding-top 12 + :align-self :flex-end + :height 64}) + +(defn view-button-container + [keyboard-shown?] + (merge button-container + (if platform/ios? + {:margin-bottom (if keyboard-shown? 0 34)} + {:margin-bottom (if keyboard-shown? 12 34)}))) + +(def blur-button-container + (merge button-container + (when platform/android? {:padding-bottom 12}))) + +(def page-container + {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0 + :z-index 100}) + +(def info-message + {:margin-top 8}) + +(def title + {:color colors/white + :margin-top 12 + :margin-bottom 18}) + +(def color-title + {:color colors/white-70-blur + :margin-top 20 + :margin-bottom 16}) + +(def content-container + {:padding-horizontal 20}) + +(def input-container + {:align-items :flex-start}) + +(def profile-input-container + {:flex-direction :row + :justify-content :center}) diff --git a/src/status_im/contexts/profile/edit/modal/view.cljs b/src/status_im/contexts/profile/edit/modal/view.cljs new file mode 100644 index 0000000000..67858f6d36 --- /dev/null +++ b/src/status_im/contexts/profile/edit/modal/view.cljs @@ -0,0 +1,266 @@ +(ns status-im.contexts.profile.edit.modal.view + (:require + [clojure.string :as string] + [oops.core :as oops] + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.hooks :as hooks] + [react-native.platform :as platform] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.avatar-picture-picker.view :as profile-picture-picker] + [status-im.common.events-helper :as events-helper] + [status-im.common.validation.profile :as profile-validator] + [status-im.constants :as c] + [status-im.contexts.profile.edit.modal.events] + [status-im.contexts.profile.edit.modal.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.responsiveness :as responsiveness])) + +(def scroll-view-height (reagent/atom 0)) +(def content-container-height (reagent/atom 0)) +(def set-scroll-view-height #(reset! scroll-view-height %)) + +(defn set-content-container-height + [e] + (reset! content-container-height (oops/oget e "nativeEvent.layout.height"))) + +(defn show-button-background? + [keyboard-height keyboard-shown content-scroll-y] + (let [button-container-height 64 + keyboard-view-height (+ keyboard-height button-container-height)] + (when keyboard-shown + (cond + platform/android? + (< (- @scroll-view-height button-container-height) @content-container-height) + + platform/ios? + (< (- @scroll-view-height keyboard-view-height) (- @content-container-height content-scroll-y)) + + :else + false)))) + +(defn button-container + [show-keyboard? keyboard-shown show-background? keyboard-height children] + (let [height (reagent/atom 0)] + (reset! height (if show-keyboard? (if keyboard-shown keyboard-height 0) 0)) + [rn/view {:style {:margin-top :auto}} + (cond + (and (> @height 0) show-background?) + [quo/blur + (when keyboard-shown + {:blur-amount 34 + :blur-type :transparent + :overlay-color :transparent + :background-color (if platform/android? + colors/neutral-100 + colors/neutral-80-opa-1-blur) + :style style/blur-button-container}) + children] + + (and (> @height 0) (not show-background?)) + [rn/view {:style (style/view-button-container true)} + children] + + (not show-keyboard?) + [rn/view {:style (style/view-button-container false)} + children])])) + +(defn- add-keyboard-listener + [e callback] + (oops/ocall rn/keyboard "addListener" e callback)) + +(defn- on-keyboard-show + [f] + (add-keyboard-listener (if platform/android? "keyboardDidShow" "keyboardWillShow") f)) + +(defn- on-keyboard-hide + [f] + (add-keyboard-listener (if platform/android? "keyboardDidHide" "keyboardWillHide") f)) + +(defn floating-button + [{:keys [custom-color scroll-y on-submit disabled?]}] + (reagent/with-let [show-keyboard? (reagent/atom false) + show-listener (on-keyboard-show #(reset! show-keyboard? true)) + hide-listener (on-keyboard-hide #(reset! show-keyboard? false))] + (let [{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard) + show-background? (show-button-background? keyboard-height + keyboard-shown + scroll-y)] + [rn/keyboard-avoiding-view + {:style {:position :absolute + :top 0 + :bottom 0 + :left 0 + :right 0} + :pointer-events :box-none} + [button-container @show-keyboard? keyboard-shown show-background? keyboard-height + [quo/button + {:accessibility-label :submit-create-profile-button + :type :primary + :customization-color custom-color + :on-press on-submit + :container-style style/continue-button + :disabled? disabled?} + (i18n/label :t/continue)]]]) + (finally + (oops/ocall show-listener "remove") + (oops/ocall hide-listener "remove")))) + +(defn- content + [{:keys [set-display-name set-validation-msg picture-picker]}] + (let [open-picture-picker (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:show-bottom-sheet + {:content picture-picker + :shell? true + :theme :dark}])) + on-change-text (fn [s] + (set-validation-msg s) + (set-display-name s))] + (fn [{:keys [profile-image custom-color display-name validation-msg valid-name? + name-too-short? initial-display-name set-scroll set-scroll-height + set-custom-color]}] + (let [{window-width :width} (rn/get-window) + info-type (cond + validation-msg :error + name-too-short? :default + :else :success) + info-message (or validation-msg + (i18n/label :t/minimum-characters + {:min-chars profile-validator/min-length})) + full-name (if (seq display-name) + display-name + initial-display-name)] + [rn/scroll-view + {:on-layout set-scroll-height + :on-scroll set-scroll + :scroll-event-throttle 64 + :content-container-style {:flex-grow 1}} + [rn/view {:on-layout set-content-container-height} + [rn/view {:style style/content-container} + [quo/text + {:style style/title + :size :heading-1 + :weight :semi-bold} + (i18n/label :t/edit-profile)] + [rn/view {:style style/input-container} + [rn/view {:style style/profile-input-container} + [quo/profile-input + {:customization-color custom-color + :placeholder initial-display-name + :on-press open-picture-picker + :image-picker-props {:profile-picture profile-image + :full-name full-name + :customization-color custom-color} + :title-input-props {:default-value "" + :auto-focus true + :max-length c/profile-name-max-length + :on-change-text on-change-text}}]] + [quo/info-message + {:status info-type + :size :default + :icon (if valid-name? :i/positive-state :i/info) + :color (when (= :default info-type) colors/white-70-blur) + :container-style style/info-message} + info-message] + [quo/text + {:size :paragraph-2 + :weight :medium + :style style/color-title} + (i18n/label :t/colour)]]] + [quo/color-picker + {:blur? true + :default-selected custom-color + :on-change set-custom-color + :window-width window-width + :container-style {:padding-left (responsiveness/iphone-11-Pro-20-pixel-from-width + window-width)}}]]])))) + +(defn- submittable-state? + [{:keys [new-color? new-picture? new-name? valid-name?]}] + (let [acceptable-name? (or (not new-name?) valid-name?)] + (or (and new-color? acceptable-name?) + (and new-picture? acceptable-name?) + (and new-name? valid-name?)))) + +(defn view + [] + (let [{:keys [pending-event]} (rf/sub [:get-screen-params]) + {initial-display-name :display-name + initial-color :customization-color + [initial-image] :images} (rf/sub [:profile/profile]) + top (safe-area/get-top) + scroll-y (reagent/atom 0) + display-name (reagent/atom "") + validation-msg (reagent/atom (profile-validator/validation-name @display-name)) + custom-color (reagent/atom (or initial-color c/profile-default-color)) + profile-image (reagent/atom initial-image) + set-display-name #(reset! validation-msg (profile-validator/validation-name %)) + set-validation-msg #(reset! display-name (string/trim %)) + set-custom-color #(reset! custom-color %) + set-scroll (fn [e] + (reset! scroll-y (oops/oget e "nativeEvent.contentOffset.y"))) + set-scroll-height (fn [e] + (reset! scroll-y 0) + (-> e + (oops/oget "nativeEvent.layout.height") + set-scroll-view-height)) + picture-picker (fn [] + [profile-picture-picker/view + {:on-result #(reset! profile-image %) + :has-picture? (some? @profile-image)}]) + update-profile (fn [] ;; TODO: check image not updated in communities and + ;; profile + (rf/dispatch + [:profile/edit-profile + (cond-> {:on-success pending-event} + (and (seq @display-name) + (not= initial-display-name @display-name)) + (assoc :display-name @display-name) + + (and (not= initial-image @profile-image)) + (assoc :picture @profile-image) + + (not= initial-color @custom-color) + (assoc :color @custom-color))])) + on-close (fn [] + (events-helper/navigate-back) + (rf/dispatch pending-event))] + (fn [] + (let [name-too-short? (profile-validator/name-too-short? @display-name) + valid-name? (and (not @validation-msg) (not name-too-short?)) + disabled? (not + (submittable-state? + {:new-color? (not= initial-color @custom-color) + :new-picture? (not= @profile-image initial-image) + :new-name? (and (not= initial-display-name @display-name) + (seq @display-name)) + :valid-name? valid-name?}))] + [rn/view {:style style/page-container} + [quo/page-nav + {:margin-top top + :background :blur + :icon-name :i/close + :on-press on-close}] + [content + {:profile-image @profile-image + :custom-color @custom-color + :display-name @display-name + :validation-msg @validation-msg + :valid-name? valid-name? + :name-too-short? name-too-short? + :picture-picker picture-picker + :initial-display-name initial-display-name + :set-scroll set-scroll + :set-scroll-height set-scroll-height + :set-custom-color set-custom-color + :set-display-name set-display-name + :set-validation-msg set-validation-msg}] + [floating-button + {:custom-color @custom-color + :scroll-y @scroll-y + :on-submit update-profile + :disabled? disabled?}]]))))