From 12aa20f467595dd5ef225247c176cdc2a760349d Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Thu, 14 May 2020 10:45:44 +0300 Subject: [PATCH] Add bottom sheet EXPERIMENTAL: uses reanimated lib so we can use reanimated buttons inside and have simultaneous handlers Add react hooks Use hooks mocks Use timing for drag transition Use view on android Signed-off-by: Gheorghe Pinzaru --- .clj-kondo/config.edn | 3 +- ios/Podfile.lock | 4 +- mobile/js_files/package.json | 3 +- mobile/js_files/yarn.lock | 13 +- src/mocks/js_dependencies.cljs | 90 ++++++---- src/quo/animated.cljs | 85 ++++++++- src/quo/components/bottom_sheet/style.cljs | 45 +++++ src/quo/components/bottom_sheet/view.cljs | 191 +++++++++++++++++++++ src/quo/components/safe_area.cljs | 11 +- src/quo/core.cljs | 4 +- src/quo/design_system/colors.cljs | 4 +- src/quo/previews/bottom_sheet.cljs | 66 +++++++ src/quo/previews/main.cljs | 6 +- src/quo/react.clj | 28 +++ src/quo/react.cljs | 123 ++++++++++++- src/quo/react_native.cljs | 21 ++- src/status_im/ui/components/react.cljs | 4 +- src/status_im/ui/screens/views.cljs | 2 +- 18 files changed, 639 insertions(+), 64 deletions(-) create mode 100644 src/quo/components/bottom_sheet/style.cljs create mode 100644 src/quo/components/bottom_sheet/view.cljs create mode 100644 src/quo/previews/bottom_sheet.cljs create mode 100644 src/quo/react.clj diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 242c95e261..59029b199d 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,11 +1,12 @@ {:lint-as {status-im.utils.views/defview clojure.core/defn status-im.utils.views/letsubs clojure.core/let status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all + quo.react/with-deps-check clojure.core/fn quo.previews.preview/list-comp clojure.core/for status-im.utils.styles/def clojure.core/def status-im.utils.styles/defn clojure.core/defn taoensso.tufte/defnp clojure.core/defn} - :linters {:invalid-arity {:skip-args [status-im.utils.fx/defn]} + :linters {:invalid-arity {:skip-args [status-im.utils.fx/defn]} ;;TODO remove number when this is fixed ;;https://github.com/borkdude/clj-kondo/issues/867 :unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 16472cb0bb..91b5e4874d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -251,7 +251,7 @@ PODS: - React - react-native-netinfo (4.7.0): - React - - react-native-safe-area-context (0.7.3): + - react-native-safe-area-context (2.0.0): - React - react-native-shake (3.4.0): - React @@ -585,7 +585,7 @@ SPEC CHECKSUMS: react-native-image-resizer: 4516052af6ae0248caf4ccf356caecefe60072d7 react-native-mail: 7e37dfbe93ff0d4c7df346b738854dbed533e86f react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9 - react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392 + react-native-safe-area-context: 60f654e00b6cc416573f6d5dbfce3839958eb57a react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 react-native-webview: cf5527893252b3b036eea024a1da6996f7344c74 diff --git a/mobile/js_files/package.json b/mobile/js_files/package.json index 9519db9848..55b6a485b3 100644 --- a/mobile/js_files/package.json +++ b/mobile/js_files/package.json @@ -12,6 +12,7 @@ "dependencies": { "@react-native-community/cameraroll": "^1.6.1", "@react-native-community/clipboard": "^1.2.2", + "@react-native-community/hooks": "^2.5.1", "@react-native-community/masked-view": "^0.1.6", "@react-native-community/netinfo": "^4.4.0", "@react-navigation/bottom-tabs": "^5.1.1", @@ -47,7 +48,7 @@ "react-native-navigation-twopane": "git+https://github.com/status-im/react-native-navigation-twopane.git#v0.0.2-status", "react-native-reanimated": "^1.7.0", "react-native-redash": "^14.0.3", - "react-native-safe-area-context": "^0.7.3", + "react-native-safe-area-context": "^2.0.0", "react-native-screens": "^2.3.0", "react-native-shake": "^3.3.1", "react-native-splash-screen": "^3.2.0", diff --git a/mobile/js_files/yarn.lock b/mobile/js_files/yarn.lock index f8a043077b..a2074bac1a 100644 --- a/mobile/js_files/yarn.lock +++ b/mobile/js_files/yarn.lock @@ -1314,6 +1314,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/clipboard/-/clipboard-1.2.2.tgz#956b29df141199fd9ed47e820baf9693f9f50b55" integrity sha512-WJkJSWA/fhuBhHL3rh6UDdB8+AZNMvAHoyo/ERnNxl9KZqruq7K+AIaQQlggEAsIVBIhjKt65fT+Zynj7gF8Cg== +"@react-native-community/hooks@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.5.1.tgz#545c76d1a6203532a8e776578bbaaa64bb754cf6" + integrity sha512-P9gwIUGpa/h8p5ASwY8QFTthXw/e/rt4mzZRfe3Xh5L13mTuOFXsYVwe9f8JAUx512cUKUsdTg6Dsg3/jTlxeg== + "@react-native-community/masked-view@^0.1.6": version "0.1.9" resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.9.tgz#383aca2fb053e3e14405c99cce2d5805df730821" @@ -6557,10 +6562,10 @@ react-native-redash@^14.0.3: normalize-svg-path "^1.0.1" parse-svg-path "^0.1.2" -react-native-safe-area-context@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-0.7.3.tgz#ad6bd4abbabe195332c53810e4ce5851eb21aa2a" - integrity sha512-9Uqu1vlXPi+2cKW/CW6OnHxA76mWC4kF3wvlqzq4DY8hn37AeiXtLFs2WkxH4yXQRrnJdP6ivc65Lz+MqwRZAA== +react-native-safe-area-context@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-2.0.0.tgz#7ef48e5a83a1e2f7fe9d5321493822b6765fd1ab" + integrity sha512-5VtCI3Nluzm7QfTcB/3j4YeWqt25QO1u5KTA1jEg1ckJzV19qCZFyHIpCCkS5+VEX+2JEHfdczhCdwE5sPgyEw== react-native-screens@^2.3.0: version "2.5.0" diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index 3bc821c8c8..c5741efc0e 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -18,46 +18,53 @@ (clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])} :ReanimatedModule {:configureProps (fn [])}} - :View {} - :FlatList {} - :Text {} - :ProgressBarAndroid {} - :StatusBar {} - :ScrollView {} - :KeyboardAvoidingView {} - :TextInput {} - :Image {} - :Picker {:Item {}} - :Switch {} - :Modal {} - :Keyboard {:dismiss (fn [])} - :Linking {} - :TouchableWithoutFeedback {} - :TouchableHighlight {} - :TouchableOpacity {} - :ActivityIndicator {} - :StyleSheet {:create (fn [])} - :Animated {:createAnimatedComponent identity - :Value (fn []) - :ValueXY (fn []) - :View {} - :FlatList {} - :ScrollView {} - :Text {}} - :Easing {:bezier (fn []) - :poly (fn []) - :out (fn []) - :in (fn []) - :inOut (fn [])} - :DeviceEventEmitter {:addListener (fn [])} - :Dimensions {:get (fn [])} - :Platform {:select (fn [])} - :I18nManager {:isRTL ""} - :NativeEventEmitter (fn []) - :requireNativeComponent (fn [] {:propTypes ""})})) + :View {} + :FlatList {} + :Text {} + :ProgressBarAndroid {} + :StatusBar {} + :ScrollView {} + :KeyboardAvoidingView {} + :TextInput {} + :Image {} + :Picker {:Item {}} + :Switch {} + :Modal {} + :Keyboard {:dismiss (fn [])} + :Linking {} + :TouchableWithoutFeedback {} + :TouchableHighlight {} + :TouchableOpacity {} + :ActivityIndicator {} + :StyleSheet {:create (fn [])} + :Animated {:createAnimatedComponent identity + :Value (fn []) + :ValueXY (fn []) + :View {} + :FlatList {} + :ScrollView {} + :Text {}} + :Easing {:bezier (fn []) + :poly (fn []) + :out (fn []) + :in (fn []) + :inOut (fn [])} + :DeviceEventEmitter {:addListener (fn [])} + :Dimensions {:get (fn [])} + :useWindowDimensions {} + :Platform {:select (fn [])} + :I18nManager {:isRTL ""} + :NativeEventEmitter (fn []) + :LayoutAnimation {:Presets {:easeInEaseOut nil + :linear nil + :spring nil} + :configureNext (fn [])} + :requireNativeComponent (fn [] {:propTypes ""})})) (set! js/ReactNative react-native) +(def reanimated-bottom-sheet #js {:default #js {}}) + (def vector-icons #js {:default #js {}}) (def webview #js {:WebView #js {}}) (def status-keycard #js {:default #js {}}) @@ -88,7 +95,7 @@ (def net-info #js {}) (def touchid #js {}) (def safe-area-context (clj->js {:SafeAreaProvider {:_reactNativeIphoneXHelper {:getStatusBarHeight (fn [])}} - :SafeAreaConsumer {} + :SafeAreaInsetsContext {:Consumer (fn [])} :SafeAreaView {}})) (def react-native-dark-mode #js {"eventEmitter" {} "initialMode" {}}) @@ -112,7 +119,12 @@ (def react-native-reanimated #js {:default #js {:createAnimatedComponent identity :eq nil :greaterOrEq nil + :greaterThan nil + :lessThan nil + :lessOrEq nil :add nil + :diff nil + :divide nil :sub nil :multiply nil :abs nil @@ -190,5 +202,7 @@ "react-native-fs" fs "react-native-mail" react-native-mail "react-native-image-resizer" image-resizer + "react-native-haptic-feedback" react-native-haptic-feedback + "reanimated-bottom-sheet" reanimated-bottom-sheet "./fleets.js" default-fleets nil)) diff --git a/src/quo/animated.cljs b/src/quo/animated.cljs index 6c0e540d8d..7dc4ee764f 100644 --- a/src/quo/animated.cljs +++ b/src/quo/animated.cljs @@ -1,25 +1,44 @@ (ns quo.animated - (:refer-clojure :exclude [set]) + (:refer-clojure :exclude [set divide]) (:require [reagent.core :as reagent] + [quo.gesture-handler :as gh] [oops.core :refer [oget ocall]] ["react-native-reanimated" :default animated :refer (clockRunning Easing)] - ["react-native-redash" :as redash])) + ["react-native-redash" :as redash] + quo.react) + (:require-macros [quo.react :refer [maybe-js-deps]])) (def view (reagent/adapt-react-class (.-View animated))) (def text (reagent/adapt-react-class (.-Text animated))) (def scroll-view (reagent/adapt-react-class (.-ScrollView animated))) (def code (reagent/adapt-react-class (.-Code animated))) +(def useCode (.-useCode animated)) + +(defn code! + ([setup-fn] + (useCode + (fn [] (setup-fn)))) + ([setup-fn deps] + (useCode + (fn [] (setup-fn)) + (maybe-js-deps deps)))) + (def eq (oget animated "eq")) (def neq (oget animated "neq")) +(def greater (oget animated "greaterThan")) (def greater-or-eq (oget animated "greaterOrEq")) +(def less (oget animated "lessThan")) +(def less-or-eq (oget animated "lessOrEq")) (def not* (oget animated "not")) (def or* (oget animated "or")) (def and* (oget animated "and")) +(def diff (oget animated "diff")) (def add (oget animated "add")) (def sub (oget animated "sub")) (def multiply (oget animated "multiply")) +(def divide (oget animated "divide")) (def abs (oget animated "abs")) (def min* (oget animated "min")) @@ -66,13 +85,13 @@ (defn cond* ([condition node] - (ocall animated "cond" + (.cond ^js animated condition (if (vector? node) (clj->js node) node))) ([condition if-node else-node] - (ocall animated "cond" + (.cond ^js animated condition (if (vector? if-node) (clj->js if-node) @@ -109,6 +128,15 @@ (defn with-spring [config] (ocall redash "withSpring" (clj->js config))) +(defn with-decay [config] + (.withDecay ^js redash (clj->js config))) + +(defn with-offset [config] + (.withOffset ^js redash (clj->js config))) + +(defn diff-clamp [node min max] + (.diffClamp ^js redash node min max)) + (defn with-spring-transition [val config] (.withSpringTransition ^js redash val (clj->js config))) @@ -116,7 +144,10 @@ (.withTimingTransition ^js redash val (clj->js config))) (defn re-timing [config] - (ocall redash "timing" (clj->js config))) + (.timing ^js redash (clj->js config))) + +(defn re-spring [config] + (.spring ^js redash (clj->js config))) (defn on-scroll [opts] (ocall redash "onScrollEvent" (clj->js opts))) @@ -131,3 +162,47 @@ (defn loop* [opts] (ocall redash "loop" (clj->js opts))) + +(defn use-value [value] + (.useValue ^js redash value)) + +(defn use-clock [] + (.useClock ^js redash)) + +(defn snap-point [value velocity snap-points] + (.snapPoint ^js redash value velocity (to-array snap-points))) + +(defn with-easing + [{val :value + :keys [snap-points velocity offset state easing duration on-snap] + :or {duration 250 + easing (:ease-out easings)}}] + (let [position (value 0) + c (clock) + animation-over (value 1) + interrupted (and* (eq state (:began gh/states)) + (clock-running c)) + vel (multiply velocity 1.5) + to (snap-point position vel snap-points) + finish-animation [(set offset position) + (stop-clock c) + (call* [position] on-snap) + (set animation-over 1)]] + (block + [(cond* interrupted finish-animation) + (cond* animation-over + (set position offset)) + (cond* (neq state (:end gh/states)) + [(set animation-over 0) + (set position (add offset val))]) + (cond* (and* (eq state (:end gh/states)) + (not* animation-over)) + [(set position (re-timing + {:clock c + :easing easing + :duration duration + :from position + :to to})) + (cond* (not* (clock-running c)) + finish-animation)]) + position]))) diff --git a/src/quo/components/bottom_sheet/style.cljs b/src/quo/components/bottom_sheet/style.cljs new file mode 100644 index 0000000000..67d3025701 --- /dev/null +++ b/src/quo/components/bottom_sheet/style.cljs @@ -0,0 +1,45 @@ +(ns quo.components.bottom-sheet.style + (:require [quo.design-system.colors :as colors] + [quo.design-system.spacing :as spacing])) + +(def border-radius 16) +(def vertical-padding (:tiny spacing/spacing)) +(def margin-top 56) + +(def container + {:position :absolute + :left 0 + :top 0 + :right 0 + :bottom 0 + :flex 1 + :justify-content :flex-end}) + +(defn backdrop [] + {:flex 1 + :position :absolute + :left 0 + :top 0 + :right 0 + :bottom 0 + :background-color (:backdrop @colors/theme)}) + +(defn content-container + [window-height] + {:background-color (:ui-background @colors/theme) + :border-top-left-radius border-radius + :border-top-right-radius border-radius + :height (* window-height 2)}) + +(def content-header + {:height border-radius + :align-self :stretch + :justify-content :center + :align-items :center}) + +(def handle + {:width 31 + :height 4 + :background-color (:icon-02 @colors/theme) + :opacity 0.4 + :border-radius 2}) diff --git a/src/quo/components/bottom_sheet/view.cljs b/src/quo/components/bottom_sheet/view.cljs new file mode 100644 index 0000000000..94100ec41e --- /dev/null +++ b/src/quo/components/bottom_sheet/view.cljs @@ -0,0 +1,191 @@ +(ns quo.components.bottom-sheet.view + (:require [reagent.core :as reagent] + [quo.animated :as animated] + [quo.react-native :as rn] + [quo.react :as react] + [quo.platform :as platform] + [cljs-bean.core :as bean] + [quo.components.safe-area :as safe-area] + [quo.components.bottom-sheet.style :as styles] + [quo.gesture-handler :as gesture-handler])) + +(def opacity-coeff 0.8) +(def close-duration 150) +(def spring-config {:damping 15 + :mass 0.7 + :stiffness 150 + :overshootClamping false + :restSpeedThreshold 0.1 + :restDisplacementThreshold 0.1}) + +;; NOTE(Ferossgp): RNGH does not work in modal react native +(defn modal [{:keys [visible] :as props} & children] + (if platform/android? + (when visible (into [:<>] children)) + (into [rn/modal props] children))) + +(defn bottom-sheet-raw [props] + (let [{on-cancel :onCancel + disable-drag? :disableDrag? + show-handle? :showHandle? + visible? :visible? + backdrop-dismiss? :backdropDismiss? + back-button-cancel :backButtonCancel + children :children + :or {show-handle? true + backdrop-dismiss? true + back-button-cancel true}} + (bean/bean props) + + {window-height :height} (rn/use-window-dimensions) + safe-area (safe-area/use-safe-area) + max-height (- window-height (:top safe-area) styles/margin-top) + content-height (react/state 0) + visible (react/state false) + + on-close (fn [] + (when @visible + (reset! visible false) + (when on-cancel (on-cancel)))) + + master-translation-y (animated/use-value 0) + master-velocity-y (animated/use-value (:undetermined gesture-handler/states)) + master-state (animated/use-value (:undetermined gesture-handler/states)) + tap-state (animated/use-value 0) + manual-open (animated/use-value 0) + manual-close (animated/value 0) + offset (animated/use-value 0) + clock (animated/use-clock) + body-ref (react/create-ref) + master-ref (react/create-ref) + tap-gesture-handler (animated/on-gesture {:state tap-state}) + on-master-event (animated/event [{:nativeEvent + {:translationY master-translation-y + :state master-state + :velocityY master-velocity-y}}]) + on-body-event on-master-event + sheet-height (min max-height @content-height) + open-snap-point (* -1 sheet-height) + close-snap-point 0 + close-sheet (fn [] + (animated/set-value manual-close 1)) + open-sheet (fn [] + (animated/set-value manual-open 1)) + on-snap (fn [pos] + (when (= close-snap-point (aget pos 0)) + (on-close))) + resistance (animated/cond* (animated/less-or-eq master-translation-y 0) + (animated/divide master-translation-y 2) + master-translation-y) + translate-y (animated/with-easing + {:value resistance + :velocity master-velocity-y + :offset offset + :state master-state + :on-snap on-snap + :snap-points [open-snap-point close-snap-point]}) + opacity (animated/cond* + open-snap-point + (animated/interpolate + translate-y + {:inputRange [(animated/multiply open-snap-point opacity-coeff) 0] + :outputRange [1 0] + :extrapolate (:clamp animated/extrapolate)})) + on-layout (fn [evt] + (->> ^js evt + .-nativeEvent + .-layout + .-height + (+ styles/border-radius) + (reset! content-height)) + (js/requestAnimationFrame open-sheet))] + (animated/code! + (fn [] + (animated/block + [(animated/on-change tap-state + [(animated/cond* (animated/and* (animated/eq tap-state (:end gesture-handler/states)) + (animated/not* manual-close)) + [(animated/set manual-close 1) + (animated/set tap-state (:undetermined gesture-handler/states))])]) + (animated/cond* manual-open + [(animated/set offset + (animated/re-spring {:from offset + :to open-snap-point + :clock clock + :config spring-config})) + (animated/cond* (animated/not* (animated/clock-running clock)) + (animated/set manual-open 0))]) + (animated/cond* (animated/and* manual-close + (animated/not* manual-open)) + [(animated/set offset + (animated/re-timing {:from offset + :to close-snap-point + :clock clock + :easing (:ease-out animated/easings) + :duration close-duration})) + (animated/cond* (animated/not* (animated/clock-running clock)) + [(animated/set manual-close 0) + (animated/call* [] on-close)])])])) + [open-snap-point on-close]) + ;; NOTE(Ferossgp): Remove me when RNGH will suport modal + (rn/use-back-handler + (fn [] + (when back-button-cancel + (close-sheet)) + true)) + (react/effect! + (fn [] + (cond + visible? + (do + (rn/dismiss-keyboard!) + (reset! visible visible?)) + + @visible + (close-sheet))) + [visible?]) + (reagent/as-element + [rn/modal {:visible @visible + :transparent true + :status-bar-translucent true + :presentation-style :overFullScreen + :hardware-accelerated true + :on-request-close (fn [] + (when back-button-cancel + (close-sheet)))} + [rn/view {:style styles/container + :pointer-events :box-none} + [gesture-handler/tap-gesture-handler (merge {:enabled backdrop-dismiss?} + tap-gesture-handler) + [animated/view {:style (merge (styles/backdrop) + {:opacity opacity})}]] + [animated/view {:style (merge (styles/content-container window-height) + {:transform [{:translateY translate-y} + {:translateY (* window-height 2)}]})} + [gesture-handler/pan-gesture-handler {:ref master-ref + :wait-for body-ref + :enabled (not disable-drag?) + :onGestureEvent on-master-event + :onHandlerStateChange on-master-event} + [animated/view {:style styles/content-header} + (when show-handle? + [rn/view {:style styles/handle}])]] + [gesture-handler/pan-gesture-handler {:ref body-ref + :wait-for master-ref + :enabled (and (not disable-drag?) + (not= sheet-height max-height)) + :onGestureEvent on-body-event + :onHandlerStateChange on-body-event} + [animated/view {:pointer-events :box-none + :height sheet-height} + [animated/scroll-view {:bounces false + :flex 1 + :scroll-enabled (= sheet-height max-height)} + [animated/view {:style {:padding-top styles/vertical-padding + :padding-bottom (+ styles/vertical-padding + (:bottom safe-area))} + :on-layout on-layout} + (into [:<>] (react/get-children children))]]]]]]]))) + +(defn bottom-sheet [props & children] + (into [:> bottom-sheet-raw props] children)) diff --git a/src/quo/components/safe_area.cljs b/src/quo/components/safe_area.cljs index 94e391dfc1..a1fa415ec5 100644 --- a/src/quo/components/safe_area.cljs +++ b/src/quo/components/safe_area.cljs @@ -1,10 +1,10 @@ (ns quo.components.safe-area (:require ["react-native-safe-area-context" :as safe-area-context - :refer (SafeAreaView SafeAreaProvider SafeAreaConsumer)] + :refer (SafeAreaView SafeAreaProvider SafeAreaInsetsContext useSafeAreaInsets)] [reagent.core :as reagent])) (def provider (reagent/adapt-react-class SafeAreaProvider)) -(def ^:private consumer-raw (reagent/adapt-react-class SafeAreaConsumer)) +(def ^:private consumer-raw (reagent/adapt-react-class (.-Consumer ^js SafeAreaInsetsContext))) (def view (reagent/adapt-react-class SafeAreaView)) (defn consumer [component] @@ -12,3 +12,10 @@ (fn [insets] (reagent/as-element [component (js->clj insets :keywordize-keys true)]))]) + +(defn use-safe-area [] + (let [insets (useSafeAreaInsets)] + {:top (.-top ^js insets) + :bottom (.-bottom ^js insets) + :left (.-left ^js insets) + :right (.-right ^js insets)})) diff --git a/src/quo/core.cljs b/src/quo/core.cljs index d52295e478..3c5ae9039e 100644 --- a/src/quo/core.cljs +++ b/src/quo/core.cljs @@ -8,7 +8,8 @@ [quo.components.button.view :as button] [quo.components.list.header :as list-header] [quo.components.list.footer :as list-footer] - [quo.components.list.item :as list-item])) + [quo.components.list.item :as list-item] + [quo.components.bottom-sheet.view :as bottom-sheet])) (def text text/text) (def header header/header) @@ -19,6 +20,7 @@ (def list-header list-header/header) (def list-footer list-footer/footer) (def list-item list-item/list-item) +(def bottom-sheet bottom-sheet/bottom-sheet) (def safe-area-provider safe-area/provider) (def safe-area-consumer safe-area/consumer) (def safe-area-view safe-area/view) diff --git a/src/quo/design_system/colors.cljs b/src/quo/design_system/colors.cljs index 903274a812..1ba89f7e5f 100644 --- a/src/quo/design_system/colors.cljs +++ b/src/quo/design_system/colors.cljs @@ -41,6 +41,7 @@ :icon-04 "rgba(67,96,223,1)" ; Interactive icon :icon-05 "rgba(255,255,255,1)" ; Icons inverse on accent background :shadow-01 "rgba(0,9,26,0.12)" ; Main shadow color + :backdrop "rgba(0,0,0,0.4)" ; Backdrop for modals and bottom sheet }) (def dark-theme @@ -64,6 +65,7 @@ :icon-03 "rgba(255,255,255,0.4)" :icon-04 "rgba(97,119,229,1)" :icon-05 "rgba(20,20,20,1)" - :shadow-01 "rgba(0,0,0,0.75)"}) + :shadow-01 "rgba(0,0,0,0.75)" + :backdrop "rgba(0,0,0,0.4)"}) (def theme (reagent/atom light-theme)) diff --git a/src/quo/previews/bottom_sheet.cljs b/src/quo/previews/bottom_sheet.cljs new file mode 100644 index 0000000000..3c2a12bccb --- /dev/null +++ b/src/quo/previews/bottom_sheet.cljs @@ -0,0 +1,66 @@ +(ns quo.previews.bottom-sheet + (:require [reagent.core :as reagent] + [quo.core :as quo] + [quo.react-native :as rn] + [quo.design-system.colors :as colors] + [quo.previews.preview :as preview])) + +(def descriptor [{:label "Show handle:" + :key :show-handle? + :type :boolean} + {:label "Backdrop dismiss:" + :key :backdrop-dismiss? + :type :boolean} + {:label "Disable drag:" + :key :disable-drag? + :type :boolean} + {:label "Android back cancel:" + :key :back-button-cancel + :type :boolean} + {:label "Scrollable:" + :key :scrollable + :type :boolean}]) + +(defn cool-preview [] + (let [state (reagent/atom {:show-handle? true + :backdrop-dismiss? true + :disable-drag? false + :back-button-cancel true}) + visible (reagent/atom false) + scrollable (reagent/cursor state [:scrollable])] + (fn [] + [rn/view {:margin-bottom 50 + :padding 16} + [preview/customizer state descriptor] + [:<> + [rn/view {:style {:align-items :center + :padding 16}} + [rn/touchable-opacity {:on-press #(reset! visible true)} + [rn/view {:style {:padding-horizontal 16 + :padding-vertical 8 + :border-radius 4 + :background-color (:interactive-01 @colors/theme)}} + [quo/text {:color :secondary-inverse} + (str "Open sheet: " @visible)]]]] + + [quo/bottom-sheet (merge @state + {:visible? @visible + :on-cancel #(reset! visible false)}) + [rn/view {:style {:height (if @scrollable 1200 400) + :justify-content :center + :align-items :center}} + [rn/touchable-opacity {:on-press #(reset! visible false)} + [quo/text {:color :link} "Close"]] + [rn/touchable-opacity {:on-press #(swap! scrollable not) + :style {:padding-vertical 16}} + [quo/text {:color :link} "Toggle size"]] + [quo/text "Hello world!"]]]]]))) + +(defn preview [] + (fn [] + [rn/view {:background-color (:ui-background @colors/theme) + :flex 1} + [rn/flat-list {:flex 1 + :keyboardShouldPersistTaps :always + :header [cool-preview] + :key-fn str}]])) diff --git a/src/quo/previews/main.cljs b/src/quo/previews/main.cljs index 115cfc46e7..704c18e6ea 100644 --- a/src/quo/previews/main.cljs +++ b/src/quo/previews/main.cljs @@ -5,6 +5,7 @@ [quo.previews.tooltip :as tooltip] [quo.previews.button :as button] [quo.previews.lists :as lists] + [quo.previews.bottom-sheet :as bottom-sheet] [quo.react-native :as rn] [quo.core :as quo] [reagent.core :as reagent] @@ -29,7 +30,10 @@ :component button/preview-button} {:name :lists :instes {:top false} - :component lists/preview}]) + :component lists/preview} + {:name :bottom-sheet + :insets {:top false} + :component bottom-sheet/preview}]) (defn theme-switcher [] [rn/view {:style {:flex-direction :row diff --git a/src/quo/react.clj b/src/quo/react.clj new file mode 100644 index 0000000000..4e5ffc9582 --- /dev/null +++ b/src/quo/react.clj @@ -0,0 +1,28 @@ +(ns quo.react) + +(defmacro maybe-js-deps [deps] + `(if ~deps (into-array ~deps) js/undefined)) + +(defmacro with-deps-check [[prev-deps] f deps] + `(let [~prev-deps (quo.react/ref ~deps)] + (when (not= @~prev-deps ~deps) + (reset! ~prev-deps ~deps)) + ~f)) + +(defmacro with-effect + "Takes optional vector of dependencies and body to be executed in an effect." + [deps & body] + (let [[deps setup-fn] (if (vector? deps) + [deps body] + [nil (cons deps body)])] + `(effect! #(do ~@setup-fn) ~deps))) + +(defmacro with-layout-effect + "Takes optional vector of dependencies and body to be executed in a layout effect." + [deps & body] + (let [[deps setup-fn] (if (vector? deps) + [deps body] + [nil (cons deps body)])] + `(layout-effect! #(do ~@setup-fn) ~deps))) + + diff --git a/src/quo/react.cljs b/src/quo/react.cljs index e97c3e61ab..cee0cc3795 100644 --- a/src/quo/react.cljs +++ b/src/quo/react.cljs @@ -1,8 +1,125 @@ (ns quo.react - (:require [oops.core :refer [oget]] - ["react" :as react])) + (:refer-clojure :exclude [ref]) + (:require [oops.core :refer [oget oset!]] + ["react" :as react]) + (:require-macros [quo.react :refer [with-deps-check + maybe-js-deps]])) -(def create-ref (oget react "createRef")) +(def create-ref react/createRef) (defn current-ref [ref] (oget ref "current")) + +;; Inspired from UIX, Rum and Rumext +(defn set-ref-val! + [ref val] + (oset! ref "current" val) + val) + +(deftype StateHook [value set-value] + cljs.core/IHash + (-hash [o] (goog/getUid o)) + + cljs.core/IDeref + (-deref [o] + value) + + cljs.core/IReset + (-reset! [o new-value] + (set-value new-value)) + + cljs.core/ISwap + (-swap! [o f] + (set-value f)) + (-swap! [o f a] + (set-value #(f % a))) + (-swap! [o f a b] + (set-value #(f % a b))) + (-swap! [o f a b xs] + (set-value #(apply f % a b xs)))) + +(defn state [value] + (let [[value set-value] (react/useState value) + sh (react/useMemo #(StateHook. value set-value) #js [])] + (react/useMemo (fn [] + (set! (.-value sh) value) + (set! (.-set-value sh) set-value) + sh) + #js [value set-value]))) + +(defn use-ref [val] + (let [ref (react/useRef val)] + (reify + cljs.core/IHash + (-hash [_] (goog/getUid ref)) + + cljs.core/IDeref + (-deref [_] + (current-ref ref)) + + cljs.core/IReset + (-reset! [_ new-value] + (set-ref-val! ref new-value)) + + cljs.core/ISwap + (-swap! [_ f] + (-reset! ref (f (current-ref ref)))) + (-swap! [_ f a] + (-reset! ref (f (current-ref ref) a))) + (-swap! [_ f a b] + (-reset! ref (f (current-ref ref) a b))) + (-swap! [_ f a b xs] + (-reset! ref (apply f (current-ref ref) a b xs)))))) + +(defn ref [value] + (let [vref (use-ref value)] + (react/useMemo (fn [] vref) #js []))) + +(defn effect! + ([setup-fn] + (react/useEffect + #(let [ret (setup-fn)] + (if (fn? ret) ret js/undefined)))) + ([setup-fn deps] + (with-deps-check [prev-deps*] + (react/useEffect + (fn [] + (reset! prev-deps* deps) + (let [ret (setup-fn)] + (if (fn? ret) ret js/undefined))) + (maybe-js-deps @prev-deps*)) + deps))) + +(defn layout-effect! + ([setup-fn] + (react/useLayoutEffect + #(let [ret (setup-fn)] + (if (fn? ret) ret js/undefined)))) + ([setup-fn deps] + (with-deps-check [prev-deps*] + (react/useLayoutEffect + (fn [] + (reset! prev-deps* deps) + (let [ret (setup-fn)] + (if (fn? ret) ret js/undefined))) + (maybe-js-deps @prev-deps*)) + deps))) + +(defn callback + ([f] (react/useCallback f)) + ([f deps] + (with-deps-check [prev-deps*] + (react/useCallback f (maybe-js-deps @prev-deps*)) + deps))) + +(defn memo + ([f] (react/useMemo f)) + ([f deps] + (with-deps-check [prev-deps*] + (react/useMemo f (maybe-js-deps @prev-deps*)) + deps))) + +(defn get-children [^js children] + (->> children + (react/Children.toArray) + (into []))) diff --git a/src/quo/react_native.cljs b/src/quo/react_native.cljs index c00d944258..eaca3e5ebb 100644 --- a/src/quo/react_native.cljs +++ b/src/quo/react_native.cljs @@ -1,6 +1,7 @@ (ns quo.react-native (:require [reagent.core :as reagent] - ["react-native" :as rn])) + ["react-native" :as rn] + ["@react-native-community/hooks" :as hooks])) (def app-registry (.-AppRegistry rn)) @@ -15,9 +16,14 @@ (def touchable-opacity (reagent/adapt-react-class (.-TouchableOpacity ^js rn))) (def touchable-highlight (reagent/adapt-react-class (.-TouchableHighlight ^js rn))) - +(def touchable-without-feedback (reagent/adapt-react-class (.-TouchableWithoutFeedback ^js rn))) (def text-input (reagent/adapt-react-class (.-TextInput ^js rn))) +(def keyboard-avoiding-view (reagent/adapt-react-class (.-KeyboardAvoidingView ^js rn))) + +(def keyboard (.-Keyboard ^js rn)) +(def dismiss-keyboard! #(.dismiss ^js keyboard)) + (def ui-manager (.-UIManager ^js rn)) (def layout-animation (.-LayoutAnimation ^js rn)) @@ -53,3 +59,14 @@ (defn flat-list [props] [rn-flat-list (base-list-props props)]) + +;; Hooks + +(defn use-window-dimensions [] + (let [window (rn/useWindowDimensions)] + {:font-scale (.-fontScale window) + :height (.-height ^js window) + :scale (.-scale ^js window) + :width (.-window ^js window)})) + +(def use-back-handler (.-useBackHandler hooks)) diff --git a/src/status_im/ui/components/react.cljs b/src/status_im/ui/components/react.cljs index cf320c1805..4ad2104cf7 100644 --- a/src/status_im/ui/components/react.cljs +++ b/src/status_im/ui/components/react.cljs @@ -8,7 +8,7 @@ ["react-native" :as react-native :refer (Keyboard)] ["react-native-image-crop-picker" :default image-picker] ["react-native-safe-area-context" :as safe-area-context - :refer (SafeAreaView SafeAreaProvider SafeAreaConsumer)] + :refer (SafeAreaView SafeAreaProvider SafeAreaInsetsContext)] ["@react-native-community/clipboard" :default Clipboard]) (:require-macros [status-im.utils.views :as views])) @@ -244,6 +244,6 @@ comp))) (def safe-area-provider (reagent/adapt-react-class SafeAreaProvider)) -(def safe-area-consumer (reagent/adapt-react-class SafeAreaConsumer)) +(def safe-area-consumer (reagent/adapt-react-class (.-Consumer ^js SafeAreaInsetsContext))) (def safe-area-view (reagent/adapt-react-class SafeAreaView)) diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index e77384a00b..a8cbd5daab 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -93,7 +93,7 @@ (defn main [] (reagent/create-class - {:component-did-mount utils.universal-links/initialize + {:component-did-mount utils.universal-links/initialize :component-will-unmount utils.universal-links/finalize :reagent-render (fn []