diff --git a/src/js/bottom_sheet.js b/src/js/bottom_sheet.js new file mode 100644 index 0000000000..3fd3d6c481 --- /dev/null +++ b/src/js/bottom_sheet.js @@ -0,0 +1,15 @@ +import {useDerivedValue, runOnJS as reaRunOnJS, runOnJS} from "react-native-reanimated"; + +export function useTranslateY(initialTranslationY, bottomSheetDy, panY) { + return useDerivedValue(() => { + return initialTranslationY - (bottomSheetDy.value - panY.value) + }) +} + +export function useBackgroundOpacity(translateY, backgroundHeight, windowHeight) { + return useDerivedValue(() => { + const opacity = ((translateY.value - windowHeight) / -backgroundHeight) * 0.5 + + return Math.max(Math.min(opacity, 0.5), 0) + }) +} diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index c3ea317c4b..ee07cf562e 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -342,6 +342,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return ( (def shell-worklets #js {}) +(def bottom-sheet #js {}) (def record-audio-worklets #js {}) ;; Update i18n_resources.cljs @@ -392,6 +393,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return ( "react-native-svg" react-native-svg "../src/js/worklet_factory.js" worklet-factory "../src/js/shell_worklets.js" shell-worklets + "../src/js/bottom_sheet.js" bottom-sheet "../src/js/record_audio_worklets.js" record-audio-worklets "./fleets.js" default-fleets "./chats.js" default-chats diff --git a/src/quo2/foundations/colors.cljs b/src/quo2/foundations/colors.cljs index db550317b5..2e68f474f7 100644 --- a/src/quo2/foundations/colors.cljs +++ b/src/quo2/foundations/colors.cljs @@ -85,6 +85,7 @@ ;;100 with transparency (def neutral-100-opa-0 (alpha neutral-100 0)) +(def neutral-100-opa-10 (alpha neutral-100 0.1)) (def neutral-100-opa-60 (alpha neutral-100 0.6)) (def neutral-100-opa-70 (alpha neutral-100 0.7)) (def neutral-100-opa-80 (alpha neutral-100 0.8)) diff --git a/src/react_native/core.cljs b/src/react_native/core.cljs index 261ee5aeb3..bc32b2ddbc 100644 --- a/src/react_native/core.cljs +++ b/src/react_native/core.cljs @@ -67,6 +67,8 @@ (def status-bar (.-StatusBar ^js react-native)) +(def style-sheet (.-StyleSheet ^js react-native)) + (defn status-bar-height [] (.-currentHeight ^js status-bar)) diff --git a/src/react_native/hooks.cljs b/src/react_native/hooks.cljs new file mode 100644 index 0000000000..5d0c41ba09 --- /dev/null +++ b/src/react_native/hooks.cljs @@ -0,0 +1,8 @@ +(ns react-native.hooks + (:require ["@react-native-community/hooks" :as hooks])) + +(defn use-keyboard + [] + (let [kb (.useKeyboard hooks)] + {:keyboard-shown (.-keyboardShown ^js kb) + :keyboard-height (.-keyboardHeight ^js kb)})) diff --git a/src/status_im/bottom_sheet/core.cljs b/src/status_im/bottom_sheet/core.cljs index 53251e3135..5bc1eee854 100644 --- a/src/status_im/bottom_sheet/core.cljs +++ b/src/status_im/bottom_sheet/core.cljs @@ -20,5 +20,9 @@ (rf/defn hide-bottom-sheet {:events [:bottom-sheet/hide]} [{:keys [db]}] - {:hide-bottom-sheet nil - :db (assoc db :bottom-sheet/show? false)}) + {:db (assoc db :bottom-sheet/show? false)}) + +(rf/defn hide-bottom-sheet-navigation-overlay + {:events [:bottom-sheet/hide-navigation-overlay]} + [{}] + {:hide-bottom-sheet nil}) diff --git a/src/status_im/ui/screens/bottom_sheets/views.cljs b/src/status_im/ui/screens/bottom_sheets/views.cljs index 5c935aa52c..408a75b5f6 100644 --- a/src/status_im/ui/screens/bottom_sheets/views.cljs +++ b/src/status_im/ui/screens/bottom_sheets/views.cljs @@ -1,12 +1,12 @@ (ns status-im.ui.screens.bottom-sheets.views - (:require [quo.core :as quo] - [re-frame.core :as re-frame] + (:require [re-frame.core :as re-frame] [status-im.ui.screens.about-app.views :as about-app] [status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.keycard.views :as keycard] [status-im.ui.screens.mobile-network-settings.view :as mobile-network-settings] [status-im.ui.screens.multiaccounts.key-storage.views :as key-storage] [status-im.ui.screens.multiaccounts.recover.views :as recover.views] + [status-im2.common.bottom-sheet.view :as bottom-sheet] [status-im2.contexts.chat.messages.pin.list.view :as pin.list])) (defn bottom-sheet @@ -14,9 +14,7 @@ (let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet]) {:keys [content] :as opts} - (cond-> {:visible? show? - :on-cancel #(re-frame/dispatch [:bottom-sheet/hide])} - + (cond-> {:visible? show?} (map? view) (merge view) @@ -43,6 +41,6 @@ (= view :pinned-messages-list) (merge {:content pin.list/pinned-messages-list}))] - [quo/bottom-sheet opts + [bottom-sheet/bottom-sheet opts (when content [content (when options options)])])) diff --git a/src/status_im2/common/bottom_sheet/styles.cljs b/src/status_im2/common/bottom_sheet/styles.cljs new file mode 100644 index 0000000000..4c1dc45d62 --- /dev/null +++ b/src/status_im2/common/bottom_sheet/styles.cljs @@ -0,0 +1,35 @@ +(ns status-im2.common.bottom-sheet.styles + (:require [quo2.foundations.colors :as colors])) + +(def border-radius 20) + +(defn handle + [] + {:position :absolute + :top 8 + :width 32 + :height 4 + :background-color (colors/theme-colors colors/neutral-100 colors/white) + :opacity 0.1 + :border-radius 100 + :align-self :center}) + +(def backdrop + {:position :absolute + :left 0 + :right 0 + :bottom 0 + :top 0 + :background-color colors/neutral-100}) + +(defn background + [] + {:position :absolute + :left 0 + :right 0 + :top 0 + :bottom 0 + :border-top-left-radius border-radius + :border-top-right-radius border-radius + :overflow :hidden + :background-color (colors/theme-colors colors/white colors/neutral-95)}) diff --git a/src/status_im2/common/bottom_sheet/view.cljs b/src/status_im2/common/bottom_sheet/view.cljs new file mode 100644 index 0000000000..ab0fa0f456 --- /dev/null +++ b/src/status_im2/common/bottom_sheet/view.cljs @@ -0,0 +1,226 @@ +(ns status-im2.common.bottom-sheet.view + (:require [oops.core :refer [oget]] + [quo.react :as react] + [status-im2.common.bottom-sheet.styles :as styles] + [re-frame.core :as re-frame] + [react-native.background-timer :as timer] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.hooks :as hooks] + [react-native.platform :as platform] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent])) + +(def bottom-sheet-js (js/require "../src/js/bottom_sheet.js")) + +(def animation-delay 450) + +(defn with-animation + [value & [options callback]] + (reanimated/with-spring + value + (clj->js (merge {:mass 2 + :stiffness 500 + :damping 200}) + options) + callback)) + +(def content-height (reagent/atom nil)) +(def show-bottom-sheet? (reagent/atom nil)) +(def keyboard-was-shown? (reagent/atom false)) +(def expanded? (reagent/atom false)) +(def gesture-running? (reagent/atom false)) + +(defn reset-atoms + [] + (reset! show-bottom-sheet? nil) + (reset! content-height nil) + (reset! expanded? false) + (reset! keyboard-was-shown? false) + (reset! gesture-running? false)) + +(defn get-bottom-sheet-gesture + [pan-y translate-y bg-height bg-height-expanded + window-height keyboard-shown disable-drag? expandable? + show-bottom-sheet? expanded? close-bottom-sheet] + (-> (gesture/gesture-pan) + (gesture/on-start + (fn [_] + (reset! gesture-running? true) + (when (and keyboard-shown (not disable-drag?) show-bottom-sheet?) + (re-frame/dispatch [:dismiss-keyboard])))) + (gesture/on-update + (fn [evt] + (when (and (not disable-drag?) show-bottom-sheet?) + (let [max-pan-up (if (or @expanded? (not expandable?)) + 0 + (- (- bg-height-expanded bg-height))) + max-pan-down (if @expanded? + bg-height-expanded + bg-height)] + (reanimated/set-shared-value pan-y + (max + (min + (.-translationY evt) + max-pan-down) + max-pan-up)))))) + (gesture/on-end + (fn [_] + (reset! gesture-running? false) + (when (and (not disable-drag?) show-bottom-sheet?) + (let [end-pan-y (- window-height (.-value translate-y)) + expand-threshold (min (* bg-height * 1.1) (+ bg-height 50)) + collapse-threshold (max (* bg-height-expanded * 0.9) (- bg-height-expanded 50)) + should-close-bottom-sheet? (< end-pan-y (max (* bg-height 0.7) 50))] + (cond + should-close-bottom-sheet? + (close-bottom-sheet) + + (and (not @expanded?) (> end-pan-y expand-threshold)) + (reset! expanded? true) + + (and @expanded? (< end-pan-y collapse-threshold)) + (reset! expanded? false)))))))) + +(defn bottom-sheet + [props children] + (let [{on-cancel :on-cancel + disable-drag? :disable-drag? + show-handle? :show-handle? + visible? :visible? + backdrop-dismiss? :backdrop-dismiss? + expandable? :expandable? + :or {show-handle? true + backdrop-dismiss? true + expandable? false}} + props + close-bottom-sheet (fn [] + (reset! show-bottom-sheet? false) + (when (fn? on-cancel) (on-cancel)) + (timer/set-timeout + #(do + (re-frame/dispatch [:bottom-sheet/hide-navigation-overlay]) + (reset-atoms)) + animation-delay))] + [safe-area/consumer + (fn [insets] + [:f> + (fn [] + (let [{window-height :height + window-width :width} + (rn/use-window-dimensions) + {:keys [keyboard-shown]} (hooks/use-keyboard) + bg-height-expanded (- window-height (:top insets)) + bg-height (max (min @content-height bg-height-expanded) 200) + bottom-sheet-dy (reanimated/use-shared-value 0) + pan-y (reanimated/use-shared-value 0) + translate-y (.useTranslateY ^js bottom-sheet-js window-height bottom-sheet-dy pan-y) + bg-opacity + (.useBackgroundOpacity ^js bottom-sheet-js translate-y bg-height window-height) + on-content-layout (fn [evt] + (let [height (oget evt "nativeEvent" "layout" "height")] + (reset! content-height height))) + on-expanded (fn [] + (reanimated/set-shared-value bottom-sheet-dy bg-height-expanded) + (reanimated/set-shared-value pan-y 0)) + on-collapsed (fn [] + (reanimated/set-shared-value bottom-sheet-dy bg-height) + (reanimated/set-shared-value pan-y 0)) + bottom-sheet-gesture (get-bottom-sheet-gesture + pan-y + translate-y + bg-height + bg-height-expanded + window-height + keyboard-shown + disable-drag? + expandable? + show-bottom-sheet? + expanded? + close-bottom-sheet)] + + (react/effect! #(do + (cond + (and + (nil? @show-bottom-sheet?) + visible? + (some? @content-height) + (> @content-height 0)) + (reset! show-bottom-sheet? true) + + (and @show-bottom-sheet? (not visible?)) + (close-bottom-sheet))) + [@show-bottom-sheet? @content-height visible?]) + (react/effect! #(do + (when @show-bottom-sheet? + (cond + keyboard-shown + (do + (reset! keyboard-was-shown? true) + (reset! expanded? true)) + (and @keyboard-was-shown? (not keyboard-shown)) + (reset! expanded? false)))) + [@show-bottom-sheet? @keyboard-was-shown?]) + (react/effect! #(do + (when-not @gesture-running? + (cond + @show-bottom-sheet? + (if @expanded? + (do + (reanimated/set-shared-value + bottom-sheet-dy + (with-animation (+ bg-height-expanded (.-value pan-y)))) + ;; Workaround for + ;; https://github.com/software-mansion/react-native-reanimated/issues/1758#issue-817145741 + ;; withTiming/withSpring callback not working + ;; on-expanded should be called as a callback of + ;; with-animation instead, once this issue has been resolved + (timer/set-timeout on-expanded animation-delay)) + (do + (reanimated/set-shared-value + bottom-sheet-dy + (with-animation (+ bg-height (.-value pan-y)))) + ;; Workaround for + ;; https://github.com/software-mansion/react-native-reanimated/issues/1758#issue-817145741 + ;; withTiming/withSpring callback not working + ;; on-collapsed should be called as a callback of + ;; with-animation instead, once this issue has been resolved + (timer/set-timeout on-collapsed animation-delay))) + + (= @show-bottom-sheet? false) + (reanimated/set-shared-value bottom-sheet-dy (with-animation 0))))) + [@show-bottom-sheet? @expanded? @gesture-running?]) + + [:<> + [rn/touchable-without-feedback {:on-press (when backdrop-dismiss? close-bottom-sheet)} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:opacity bg-opacity} + styles/backdrop)}]] + + [gesture/gesture-detector {:gesture bottom-sheet-gesture} + [reanimated/view + {:style (reanimated/apply-animations-to-style + {:transform [{:translateY translate-y}]} + {:width window-width + :height window-height})} + [rn/view {:style (styles/background)} + [rn/keyboard-avoiding-view + {:behaviour (if platform/ios? :padding :height) + :style {:flex 1}} + [rn/view + {:style {:position :absolute + :left 0 + :right 0 + :top 0 + :padding-top styles/border-radius + :padding-bottom (:bottom insets)} + :on-layout (when-not (and + (some? @content-height) + (> @content-height 0)) + on-content-layout)} + children]] + + (when show-handle? + [rn/view {:style (styles/handle)}])]]]]))])])) diff --git a/src/status_im2/contexts/quo_preview/bottom_sheet/bottom_sheet.cljs b/src/status_im2/contexts/quo_preview/bottom_sheet/bottom_sheet.cljs new file mode 100644 index 0000000000..840ee68e86 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/bottom_sheet/bottom_sheet.cljs @@ -0,0 +1,65 @@ +(ns status-im2.contexts.quo-preview.bottom-sheet.bottom-sheet + (:require [quo2.components.buttons.button :as button] + [quo2.components.markdown.text :as text] + [quo2.foundations.colors :as colors] + [re-frame.core :as re-frame] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im2.contexts.quo-preview.preview :as preview])) + +(def descriptor + [{:label "Show handle:" + :key :show-handle? + :type :boolean} + {:label "Backdrop dismiss:" + :key :backdrop-dismiss? + :type :boolean} + {:label "Expendable:" + :key :expandable? + :type :boolean} + {:label "Disable drag:" + :key :disable-drag? + :type :boolean}]) + +(defn bottom-sheet-content + [] + [rn/view + {:style {:justify-content :center + :align-items :center}} + [button/button {:on-press #(do (re-frame/dispatch [:bottom-sheet/hide]))} "Close bottom sheet"] + + [text/text {:style {:padding-top 20}} "Hello world!"]]) + +(defn cool-preview + [] + (let [state (reagent/atom {:show-handle? true + :backdrop-dismiss? true + :expandable? true + :disable-drag? false}) + on-bottom-sheet-open (fn [] + (re-frame/dispatch [:bottom-sheet/show-sheet + (merge + {:content bottom-sheet-content} + @state)]))] + (fn [] + [rn/view + {:style {:margin-bottom 50 + :padding 16}} + [preview/customizer state descriptor] + [:<> + [rn/view + {:style {:align-items :center + :padding 16}} + + [button/button {:on-press on-bottom-sheet-open} "Open bottom sheet"]]]]))) + +(defn preview-bottom-sheet + [] + [rn/view + {:background-color (colors/theme-colors colors/white colors/neutral-90) + :flex 1} + [rn/flat-list + {:flex 1 + :keyboardShouldPersistTaps :always + :header [cool-preview] + :key-fn str}]]) diff --git a/src/status_im2/navigation/view.cljs b/src/status_im2/navigation/view.cljs index 8292585e5b..2fd4236628 100644 --- a/src/status_im2/navigation/view.cljs +++ b/src/status_im2/navigation/view.cljs @@ -126,7 +126,7 @@ (def sheet-comp (reagent/reactify-component (fn [] - ^{:key (str "seet" @reloader/cnt)} + ^{:key (str "sheet" @reloader/cnt)} [safe-area/safe-area-provider [inactive] [bottom-sheets/bottom-sheet]