feat: implement quo2 bottom sheet component (#14209)

This commit is contained in:
Christoph Pader 2022-12-28 15:23:58 +01:00 committed by GitHub
parent b45261f2e5
commit 6280a6c4d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 365 additions and 9 deletions

15
src/js/bottom_sheet.js Normal file
View File

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

View File

@ -342,6 +342,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
(def shell-worklets #js {}) (def shell-worklets #js {})
(def bottom-sheet #js {})
(def record-audio-worklets #js {}) (def record-audio-worklets #js {})
;; Update i18n_resources.cljs ;; Update i18n_resources.cljs
@ -392,6 +393,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
"react-native-svg" react-native-svg "react-native-svg" react-native-svg
"../src/js/worklet_factory.js" worklet-factory "../src/js/worklet_factory.js" worklet-factory
"../src/js/shell_worklets.js" shell-worklets "../src/js/shell_worklets.js" shell-worklets
"../src/js/bottom_sheet.js" bottom-sheet
"../src/js/record_audio_worklets.js" record-audio-worklets "../src/js/record_audio_worklets.js" record-audio-worklets
"./fleets.js" default-fleets "./fleets.js" default-fleets
"./chats.js" default-chats "./chats.js" default-chats

View File

@ -85,6 +85,7 @@
;;100 with transparency ;;100 with transparency
(def neutral-100-opa-0 (alpha neutral-100 0)) (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-60 (alpha neutral-100 0.6))
(def neutral-100-opa-70 (alpha neutral-100 0.7)) (def neutral-100-opa-70 (alpha neutral-100 0.7))
(def neutral-100-opa-80 (alpha neutral-100 0.8)) (def neutral-100-opa-80 (alpha neutral-100 0.8))

View File

@ -67,6 +67,8 @@
(def status-bar (.-StatusBar ^js react-native)) (def status-bar (.-StatusBar ^js react-native))
(def style-sheet (.-StyleSheet ^js react-native))
(defn status-bar-height (defn status-bar-height
[] []
(.-currentHeight ^js status-bar)) (.-currentHeight ^js status-bar))

View File

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

View File

@ -20,5 +20,9 @@
(rf/defn hide-bottom-sheet (rf/defn hide-bottom-sheet
{:events [:bottom-sheet/hide]} {:events [:bottom-sheet/hide]}
[{:keys [db]}] [{: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})

View File

@ -1,12 +1,12 @@
(ns status-im.ui.screens.bottom-sheets.views (ns status-im.ui.screens.bottom-sheets.views
(:require [quo.core :as quo] (:require [re-frame.core :as re-frame]
[re-frame.core :as re-frame]
[status-im.ui.screens.about-app.views :as about-app] [status-im.ui.screens.about-app.views :as about-app]
[status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.home.sheet.views :as home.sheet]
[status-im.ui.screens.keycard.views :as keycard] [status-im.ui.screens.keycard.views :as keycard]
[status-im.ui.screens.mobile-network-settings.view :as mobile-network-settings] [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.key-storage.views :as key-storage]
[status-im.ui.screens.multiaccounts.recover.views :as recover.views] [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])) [status-im2.contexts.chat.messages.pin.list.view :as pin.list]))
(defn bottom-sheet (defn bottom-sheet
@ -14,9 +14,7 @@
(let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet]) (let [{:keys [show? view options]} @(re-frame/subscribe [:bottom-sheet])
{:keys [content] {:keys [content]
:as opts} :as opts}
(cond-> {:visible? show? (cond-> {:visible? show?}
:on-cancel #(re-frame/dispatch [:bottom-sheet/hide])}
(map? view) (map? view)
(merge view) (merge view)
@ -43,6 +41,6 @@
(= view :pinned-messages-list) (= view :pinned-messages-list)
(merge {:content pin.list/pinned-messages-list}))] (merge {:content pin.list/pinned-messages-list}))]
[quo/bottom-sheet opts [bottom-sheet/bottom-sheet opts
(when content (when content
[content (when options options)])])) [content (when options options)])]))

View File

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

View File

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

View File

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

View File

@ -126,7 +126,7 @@
(def sheet-comp (def sheet-comp
(reagent/reactify-component (reagent/reactify-component
(fn [] (fn []
^{:key (str "seet" @reloader/cnt)} ^{:key (str "sheet" @reloader/cnt)}
[safe-area/safe-area-provider [safe-area/safe-area-provider
[inactive] [inactive]
[bottom-sheets/bottom-sheet] [bottom-sheets/bottom-sheet]