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 <feross95@gmail.com>
This commit is contained in:
Gheorghe Pinzaru 2020-05-14 10:45:44 +03:00
parent d3c9175514
commit 12aa20f467
No known key found for this signature in database
GPG Key ID: C9A094959935A952
18 changed files with 639 additions and 64 deletions

View File

@ -1,11 +1,12 @@
{:lint-as {status-im.utils.views/defview clojure.core/defn {:lint-as {status-im.utils.views/defview clojure.core/defn
status-im.utils.views/letsubs clojure.core/let status-im.utils.views/letsubs clojure.core/let
status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all 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 quo.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn clojure.core/defn status-im.utils.styles/defn clojure.core/defn
taoensso.tufte/defnp 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 ;;TODO remove number when this is fixed
;;https://github.com/borkdude/clj-kondo/issues/867 ;;https://github.com/borkdude/clj-kondo/issues/867
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}} :unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}}

View File

@ -251,7 +251,7 @@ PODS:
- React - React
- react-native-netinfo (4.7.0): - react-native-netinfo (4.7.0):
- React - React
- react-native-safe-area-context (0.7.3): - react-native-safe-area-context (2.0.0):
- React - React
- react-native-shake (3.4.0): - react-native-shake (3.4.0):
- React - React
@ -585,7 +585,7 @@ SPEC CHECKSUMS:
react-native-image-resizer: 4516052af6ae0248caf4ccf356caecefe60072d7 react-native-image-resizer: 4516052af6ae0248caf4ccf356caecefe60072d7
react-native-mail: 7e37dfbe93ff0d4c7df346b738854dbed533e86f react-native-mail: 7e37dfbe93ff0d4c7df346b738854dbed533e86f
react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9 react-native-netinfo: ddaca8bbb9e6e914b1a23787ccb879bc642931c9
react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392 react-native-safe-area-context: 60f654e00b6cc416573f6d5dbfce3839958eb57a
react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webview: cf5527893252b3b036eea024a1da6996f7344c74 react-native-webview: cf5527893252b3b036eea024a1da6996f7344c74

View File

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@react-native-community/cameraroll": "^1.6.1", "@react-native-community/cameraroll": "^1.6.1",
"@react-native-community/clipboard": "^1.2.2", "@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/masked-view": "^0.1.6",
"@react-native-community/netinfo": "^4.4.0", "@react-native-community/netinfo": "^4.4.0",
"@react-navigation/bottom-tabs": "^5.1.1", "@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-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-reanimated": "^1.7.0",
"react-native-redash": "^14.0.3", "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-screens": "^2.3.0",
"react-native-shake": "^3.3.1", "react-native-shake": "^3.3.1",
"react-native-splash-screen": "^3.2.0", "react-native-splash-screen": "^3.2.0",

View File

@ -1314,6 +1314,11 @@
resolved "https://registry.yarnpkg.com/@react-native-community/clipboard/-/clipboard-1.2.2.tgz#956b29df141199fd9ed47e820baf9693f9f50b55" resolved "https://registry.yarnpkg.com/@react-native-community/clipboard/-/clipboard-1.2.2.tgz#956b29df141199fd9ed47e820baf9693f9f50b55"
integrity sha512-WJkJSWA/fhuBhHL3rh6UDdB8+AZNMvAHoyo/ERnNxl9KZqruq7K+AIaQQlggEAsIVBIhjKt65fT+Zynj7gF8Cg== 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": "@react-native-community/masked-view@^0.1.6":
version "0.1.9" version "0.1.9"
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.9.tgz#383aca2fb053e3e14405c99cce2d5805df730821" 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" normalize-svg-path "^1.0.1"
parse-svg-path "^0.1.2" parse-svg-path "^0.1.2"
react-native-safe-area-context@^0.7.3: react-native-safe-area-context@^2.0.0:
version "0.7.3" version "2.0.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-0.7.3.tgz#ad6bd4abbabe195332c53810e4ce5851eb21aa2a" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-2.0.0.tgz#7ef48e5a83a1e2f7fe9d5321493822b6765fd1ab"
integrity sha512-9Uqu1vlXPi+2cKW/CW6OnHxA76mWC4kF3wvlqzq4DY8hn37AeiXtLFs2WkxH4yXQRrnJdP6ivc65Lz+MqwRZAA== integrity sha512-5VtCI3Nluzm7QfTcB/3j4YeWqt25QO1u5KTA1jEg1ckJzV19qCZFyHIpCCkS5+VEX+2JEHfdczhCdwE5sPgyEw==
react-native-screens@^2.3.0: react-native-screens@^2.3.0:
version "2.5.0" version "2.5.0"

View File

@ -18,46 +18,53 @@
(clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])} (clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])}
:ReanimatedModule {:configureProps (fn [])}} :ReanimatedModule {:configureProps (fn [])}}
:View {} :View {}
:FlatList {} :FlatList {}
:Text {} :Text {}
:ProgressBarAndroid {} :ProgressBarAndroid {}
:StatusBar {} :StatusBar {}
:ScrollView {} :ScrollView {}
:KeyboardAvoidingView {} :KeyboardAvoidingView {}
:TextInput {} :TextInput {}
:Image {} :Image {}
:Picker {:Item {}} :Picker {:Item {}}
:Switch {} :Switch {}
:Modal {} :Modal {}
:Keyboard {:dismiss (fn [])} :Keyboard {:dismiss (fn [])}
:Linking {} :Linking {}
:TouchableWithoutFeedback {} :TouchableWithoutFeedback {}
:TouchableHighlight {} :TouchableHighlight {}
:TouchableOpacity {} :TouchableOpacity {}
:ActivityIndicator {} :ActivityIndicator {}
:StyleSheet {:create (fn [])} :StyleSheet {:create (fn [])}
:Animated {:createAnimatedComponent identity :Animated {:createAnimatedComponent identity
:Value (fn []) :Value (fn [])
:ValueXY (fn []) :ValueXY (fn [])
:View {} :View {}
:FlatList {} :FlatList {}
:ScrollView {} :ScrollView {}
:Text {}} :Text {}}
:Easing {:bezier (fn []) :Easing {:bezier (fn [])
:poly (fn []) :poly (fn [])
:out (fn []) :out (fn [])
:in (fn []) :in (fn [])
:inOut (fn [])} :inOut (fn [])}
:DeviceEventEmitter {:addListener (fn [])} :DeviceEventEmitter {:addListener (fn [])}
:Dimensions {:get (fn [])} :Dimensions {:get (fn [])}
:Platform {:select (fn [])} :useWindowDimensions {}
:I18nManager {:isRTL ""} :Platform {:select (fn [])}
:NativeEventEmitter (fn []) :I18nManager {:isRTL ""}
:requireNativeComponent (fn [] {:propTypes ""})})) :NativeEventEmitter (fn [])
:LayoutAnimation {:Presets {:easeInEaseOut nil
:linear nil
:spring nil}
:configureNext (fn [])}
:requireNativeComponent (fn [] {:propTypes ""})}))
(set! js/ReactNative react-native) (set! js/ReactNative react-native)
(def reanimated-bottom-sheet #js {:default #js {}})
(def vector-icons #js {:default #js {}}) (def vector-icons #js {:default #js {}})
(def webview #js {:WebView #js {}}) (def webview #js {:WebView #js {}})
(def status-keycard #js {:default #js {}}) (def status-keycard #js {:default #js {}})
@ -88,7 +95,7 @@
(def net-info #js {}) (def net-info #js {})
(def touchid #js {}) (def touchid #js {})
(def safe-area-context (clj->js {:SafeAreaProvider {:_reactNativeIphoneXHelper {:getStatusBarHeight (fn [])}} (def safe-area-context (clj->js {:SafeAreaProvider {:_reactNativeIphoneXHelper {:getStatusBarHeight (fn [])}}
:SafeAreaConsumer {} :SafeAreaInsetsContext {:Consumer (fn [])}
:SafeAreaView {}})) :SafeAreaView {}}))
(def react-native-dark-mode #js {"eventEmitter" {} "initialMode" {}}) (def react-native-dark-mode #js {"eventEmitter" {} "initialMode" {}})
@ -112,7 +119,12 @@
(def react-native-reanimated #js {:default #js {:createAnimatedComponent identity (def react-native-reanimated #js {:default #js {:createAnimatedComponent identity
:eq nil :eq nil
:greaterOrEq nil :greaterOrEq nil
:greaterThan nil
:lessThan nil
:lessOrEq nil
:add nil :add nil
:diff nil
:divide nil
:sub nil :sub nil
:multiply nil :multiply nil
:abs nil :abs nil
@ -190,5 +202,7 @@
"react-native-fs" fs "react-native-fs" fs
"react-native-mail" react-native-mail "react-native-mail" react-native-mail
"react-native-image-resizer" image-resizer "react-native-image-resizer" image-resizer
"react-native-haptic-feedback" react-native-haptic-feedback
"reanimated-bottom-sheet" reanimated-bottom-sheet
"./fleets.js" default-fleets "./fleets.js" default-fleets
nil)) nil))

View File

@ -1,25 +1,44 @@
(ns quo.animated (ns quo.animated
(:refer-clojure :exclude [set]) (:refer-clojure :exclude [set divide])
(:require [reagent.core :as reagent] (:require [reagent.core :as reagent]
[quo.gesture-handler :as gh]
[oops.core :refer [oget ocall]] [oops.core :refer [oget ocall]]
["react-native-reanimated" :default animated :refer (clockRunning Easing)] ["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 view (reagent/adapt-react-class (.-View animated)))
(def text (reagent/adapt-react-class (.-Text animated))) (def text (reagent/adapt-react-class (.-Text animated)))
(def scroll-view (reagent/adapt-react-class (.-ScrollView animated))) (def scroll-view (reagent/adapt-react-class (.-ScrollView animated)))
(def code (reagent/adapt-react-class (.-Code 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 eq (oget animated "eq"))
(def neq (oget animated "neq")) (def neq (oget animated "neq"))
(def greater (oget animated "greaterThan"))
(def greater-or-eq (oget animated "greaterOrEq")) (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 not* (oget animated "not"))
(def or* (oget animated "or")) (def or* (oget animated "or"))
(def and* (oget animated "and")) (def and* (oget animated "and"))
(def diff (oget animated "diff"))
(def add (oget animated "add")) (def add (oget animated "add"))
(def sub (oget animated "sub")) (def sub (oget animated "sub"))
(def multiply (oget animated "multiply")) (def multiply (oget animated "multiply"))
(def divide (oget animated "divide"))
(def abs (oget animated "abs")) (def abs (oget animated "abs"))
(def min* (oget animated "min")) (def min* (oget animated "min"))
@ -66,13 +85,13 @@
(defn cond* (defn cond*
([condition node] ([condition node]
(ocall animated "cond" (.cond ^js animated
condition condition
(if (vector? node) (if (vector? node)
(clj->js node) (clj->js node)
node))) node)))
([condition if-node else-node] ([condition if-node else-node]
(ocall animated "cond" (.cond ^js animated
condition condition
(if (vector? if-node) (if (vector? if-node)
(clj->js if-node) (clj->js if-node)
@ -109,6 +128,15 @@
(defn with-spring [config] (defn with-spring [config]
(ocall redash "withSpring" (clj->js 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] (defn with-spring-transition [val config]
(.withSpringTransition ^js redash val (clj->js config))) (.withSpringTransition ^js redash val (clj->js config)))
@ -116,7 +144,10 @@
(.withTimingTransition ^js redash val (clj->js config))) (.withTimingTransition ^js redash val (clj->js config)))
(defn re-timing [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] (defn on-scroll [opts]
(ocall redash "onScrollEvent" (clj->js opts))) (ocall redash "onScrollEvent" (clj->js opts)))
@ -131,3 +162,47 @@
(defn loop* [opts] (defn loop* [opts]
(ocall redash "loop" (clj->js 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])))

View File

@ -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})

View File

@ -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))

View File

@ -1,10 +1,10 @@
(ns quo.components.safe-area (ns quo.components.safe-area
(:require ["react-native-safe-area-context" :as safe-area-context (:require ["react-native-safe-area-context" :as safe-area-context
:refer (SafeAreaView SafeAreaProvider SafeAreaConsumer)] :refer (SafeAreaView SafeAreaProvider SafeAreaInsetsContext useSafeAreaInsets)]
[reagent.core :as reagent])) [reagent.core :as reagent]))
(def provider (reagent/adapt-react-class SafeAreaProvider)) (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)) (def view (reagent/adapt-react-class SafeAreaView))
(defn consumer [component] (defn consumer [component]
@ -12,3 +12,10 @@
(fn [insets] (fn [insets]
(reagent/as-element (reagent/as-element
[component (js->clj insets :keywordize-keys true)]))]) [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)}))

View File

@ -8,7 +8,8 @@
[quo.components.button.view :as button] [quo.components.button.view :as button]
[quo.components.list.header :as list-header] [quo.components.list.header :as list-header]
[quo.components.list.footer :as list-footer] [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 text text/text)
(def header header/header) (def header header/header)
@ -19,6 +20,7 @@
(def list-header list-header/header) (def list-header list-header/header)
(def list-footer list-footer/footer) (def list-footer list-footer/footer)
(def list-item list-item/list-item) (def list-item list-item/list-item)
(def bottom-sheet bottom-sheet/bottom-sheet)
(def safe-area-provider safe-area/provider) (def safe-area-provider safe-area/provider)
(def safe-area-consumer safe-area/consumer) (def safe-area-consumer safe-area/consumer)
(def safe-area-view safe-area/view) (def safe-area-view safe-area/view)

View File

@ -41,6 +41,7 @@
:icon-04 "rgba(67,96,223,1)" ; Interactive icon :icon-04 "rgba(67,96,223,1)" ; Interactive icon
:icon-05 "rgba(255,255,255,1)" ; Icons inverse on accent background :icon-05 "rgba(255,255,255,1)" ; Icons inverse on accent background
:shadow-01 "rgba(0,9,26,0.12)" ; Main shadow color :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 (def dark-theme
@ -64,6 +65,7 @@
:icon-03 "rgba(255,255,255,0.4)" :icon-03 "rgba(255,255,255,0.4)"
:icon-04 "rgba(97,119,229,1)" :icon-04 "rgba(97,119,229,1)"
:icon-05 "rgba(20,20,20,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)) (def theme (reagent/atom light-theme))

View File

@ -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}]]))

View File

@ -5,6 +5,7 @@
[quo.previews.tooltip :as tooltip] [quo.previews.tooltip :as tooltip]
[quo.previews.button :as button] [quo.previews.button :as button]
[quo.previews.lists :as lists] [quo.previews.lists :as lists]
[quo.previews.bottom-sheet :as bottom-sheet]
[quo.react-native :as rn] [quo.react-native :as rn]
[quo.core :as quo] [quo.core :as quo]
[reagent.core :as reagent] [reagent.core :as reagent]
@ -29,7 +30,10 @@
:component button/preview-button} :component button/preview-button}
{:name :lists {:name :lists
:instes {:top false} :instes {:top false}
:component lists/preview}]) :component lists/preview}
{:name :bottom-sheet
:insets {:top false}
:component bottom-sheet/preview}])
(defn theme-switcher [] (defn theme-switcher []
[rn/view {:style {:flex-direction :row [rn/view {:style {:flex-direction :row

28
src/quo/react.clj Normal file
View File

@ -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)))

View File

@ -1,8 +1,125 @@
(ns quo.react (ns quo.react
(:require [oops.core :refer [oget]] (:refer-clojure :exclude [ref])
["react" :as react])) (: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] (defn current-ref [ref]
(oget ref "current")) (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 [])))

View File

@ -1,6 +1,7 @@
(ns quo.react-native (ns quo.react-native
(:require [reagent.core :as reagent] (:require [reagent.core :as reagent]
["react-native" :as rn])) ["react-native" :as rn]
["@react-native-community/hooks" :as hooks]))
(def app-registry (.-AppRegistry rn)) (def app-registry (.-AppRegistry rn))
@ -15,9 +16,14 @@
(def touchable-opacity (reagent/adapt-react-class (.-TouchableOpacity ^js rn))) (def touchable-opacity (reagent/adapt-react-class (.-TouchableOpacity ^js rn)))
(def touchable-highlight (reagent/adapt-react-class (.-TouchableHighlight ^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 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 ui-manager (.-UIManager ^js rn))
(def layout-animation (.-LayoutAnimation ^js rn)) (def layout-animation (.-LayoutAnimation ^js rn))
@ -53,3 +59,14 @@
(defn flat-list [props] (defn flat-list [props]
[rn-flat-list (base-list-props 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))

View File

@ -8,7 +8,7 @@
["react-native" :as react-native :refer (Keyboard)] ["react-native" :as react-native :refer (Keyboard)]
["react-native-image-crop-picker" :default image-picker] ["react-native-image-crop-picker" :default image-picker]
["react-native-safe-area-context" :as safe-area-context ["react-native-safe-area-context" :as safe-area-context
:refer (SafeAreaView SafeAreaProvider SafeAreaConsumer)] :refer (SafeAreaView SafeAreaProvider SafeAreaInsetsContext)]
["@react-native-community/clipboard" :default Clipboard]) ["@react-native-community/clipboard" :default Clipboard])
(:require-macros [status-im.utils.views :as views])) (:require-macros [status-im.utils.views :as views]))
@ -244,6 +244,6 @@
comp))) comp)))
(def safe-area-provider (reagent/adapt-react-class SafeAreaProvider)) (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)) (def safe-area-view (reagent/adapt-react-class SafeAreaView))

View File

@ -93,7 +93,7 @@
(defn main [] (defn main []
(reagent/create-class (reagent/create-class
{:component-did-mount utils.universal-links/initialize {:component-did-mount utils.universal-links/initialize
:component-will-unmount utils.universal-links/finalize :component-will-unmount utils.universal-links/finalize
:reagent-render :reagent-render
(fn [] (fn []