feat: bottom sheet screen (#15399)

* feat: bottom sheet screen
This commit is contained in:
Omar Basem 2023-03-22 17:31:20 +04:00 committed by GitHub
parent daa78b4171
commit f9255100a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 50 deletions

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-10 (alpha neutral-100 0.1))
(def neutral-100-opa-30 (alpha neutral-100 0.3))
(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

@ -6,3 +6,7 @@
(let [kb (.useKeyboard hooks)] (let [kb (.useKeyboard hooks)]
{:keyboard-shown (.-keyboardShown ^js kb) {:keyboard-shown (.-keyboardShown ^js kb)
:keyboard-height (.-keyboardHeight ^js kb)})) :keyboard-height (.-keyboardHeight ^js kb)}))
(defn use-back-handler
[handler]
(.useBackHandler hooks handler))

View File

@ -21,6 +21,8 @@
[utils.collection] [utils.collection]
[utils.worklets.core :as worklets.core])) [utils.worklets.core :as worklets.core]))
(def ^:const default-duration 300)
;; Animations ;; Animations
(def slide-in-up-animation SlideInUp) (def slide-in-up-animation SlideInUp)
(def slide-out-up-animation SlideOutUp) (def slide-out-up-animation SlideOutUp)
@ -65,6 +67,7 @@
(def in-out (def in-out
(.-inOut ^js Easing)) (.-inOut ^js Easing))
;; trying to put default-easing inside easings map causes test to fail
(defn default-easing [] (in-out (.-quad ^js Easing))) (defn default-easing [] (in-out (.-quad ^js Easing)))
(def easings (def easings
@ -115,13 +118,15 @@
(js-obj "duration" duration (js-obj "duration" duration
"easing" (get easings easing)))))) "easing" (get easings easing))))))
(defn animate-shared-value-with-delay-default-easing (defn animate-delay
[anim val duration delay] ([animation val delay]
(set-shared-value anim (animate-delay animation val delay default-duration))
([animation val delay duration]
(set-shared-value animation
(with-delay delay (with-delay delay
(with-timing val (with-timing val
(js-obj "duration" duration (clj->js {:duration duration
"easing" (in-out (.-quad ^js Easing))))))) :easing (default-easing)}))))))
(defn animate-shared-value-with-repeat (defn animate-shared-value-with-repeat
[anim val duration easing number-of-repetitions reverse?] [anim val duration easing number-of-repetitions reverse?]

View File

@ -0,0 +1,42 @@
(ns status-im2.common.bottom-sheet-screen.style
(:require
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]))
(defn background
[opacity]
(reanimated/apply-animations-to-style
{:opacity opacity}
{:background-color colors/neutral-100-opa-70
:position :absolute
:top 0
:bottom 0
:left 0
:right 0}))
(defn main-view
[translate-y]
(reanimated/apply-animations-to-style
{:transform [{:translate-y translate-y}]}
{:background-color (colors/theme-colors colors/white colors/neutral-95)
:border-top-left-radius 20
:border-top-right-radius 20
:flex 1
:overflow :hidden}))
(def handle-container
{:left 0
:right 0
:top 0
:height 20
:z-index 1
:position :absolute
:justify-content :center
:align-items :center})
(defn handle
[]
{:width 32
:height 4
:border-radius 100
:background-color (colors/theme-colors colors/neutral-100-opa-30 colors/white-opa-30)})

View File

@ -0,0 +1,81 @@
(ns status-im2.common.bottom-sheet-screen.view
(:require
[react-native.gesture :as gesture]
[react-native.hooks :as hooks]
[react-native.navigation :as navigation]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[oops.core :as oops]
[react-native.safe-area :as safe-area]
[status-im2.common.bottom-sheet-screen.style :as style]
[react-native.core :as rn]
[reagent.core :as reagent]
[utils.re-frame :as rf]))
(def ^:const drag-threshold 100)
(defn drag-gesture
[translate-y opacity scroll-enabled curr-scroll]
(->
(gesture/gesture-pan)
(gesture/on-start (fn [e]
(when (< (oops/oget e "velocityY") 0)
(reset! scroll-enabled true))))
(gesture/on-update (fn [e]
(let [translation (oops/oget e "translationY")
progress (Math/abs (/ translation drag-threshold))]
(when (pos? translation)
(reanimated/set-shared-value translate-y translation)
(reanimated/set-shared-value opacity (- 1 (/ progress 5)))))))
(gesture/on-end (fn [e]
(if (> (oops/oget e "translationY") drag-threshold)
(do
(reanimated/set-shared-value opacity (reanimated/with-timing-duration 0 100))
(rf/dispatch [:navigate-back]))
(do
(reanimated/set-shared-value opacity (reanimated/with-timing 1))
(reanimated/set-shared-value translate-y (reanimated/with-timing 0))
(reset! scroll-enabled true)))))
(gesture/on-finalize (fn [e]
(when (and (>= (oops/oget e "velocityY") 0)
(<= @curr-scroll (if platform/ios? -1 0)))
(reset! scroll-enabled false))))))
(defn on-scroll
[e curr-scroll]
(let [y (oops/oget e "nativeEvent.contentOffset.y")]
(reset! curr-scroll y)))
(defn view
[content skip-background?]
[:f>
(let [scroll-enabled (reagent/atom true)
curr-scroll (atom 0)]
(fn []
(let [sb-height (navigation/status-bar-height)
insets (safe-area/use-safe-area)
padding-top (Math/max sb-height (:top insets))
padding-top (if platform/ios? padding-top (+ padding-top 10))
opacity (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value 0)
close (fn []
(reanimated/set-shared-value opacity (reanimated/with-timing-duration 0 100))
(rf/dispatch [:navigate-back]))]
(rn/use-effect
(fn []
(reanimated/animate-delay opacity 1 (if platform/ios? 300 100))))
(hooks/use-back-handler close)
[rn/view
{:style {:flex 1
:padding-top padding-top}}
(when-not skip-background?
[reanimated/view {:style (style/background opacity)}])
[gesture/gesture-detector
{:gesture (drag-gesture translate-y opacity scroll-enabled curr-scroll)}
[reanimated/view {:style (style/main-view translate-y)}
[rn/view {:style style/handle-container}
[rn/view {:style (style/handle)}]]
[content
{:close close
:scroll-enabled @scroll-enabled
:on-scroll #(on-scroll % curr-scroll)}]]]])))])

View File

@ -41,13 +41,13 @@
:size 32} :i/reaction]]) :size 32} :i/reaction]])
(defn image-button (defn image-button
[chat-id] [insets]
[quo/button [quo/button
{:on-press (fn [] {:on-press (fn []
(permissions/request-permissions (permissions/request-permissions
{:permissions [:read-external-storage :write-external-storage] {:permissions [:read-external-storage :write-external-storage]
:on-allowed #(rf/dispatch :on-allowed #(rf/dispatch
[:open-modal :photo-selector {:chat-id chat-id}]) [:open-modal :photo-selector {:insets insets}])
:on-denied (fn [] :on-denied (fn []
(background-timer/set-timeout (background-timer/set-timeout
#(utils-old/show-popup (i18n/label :t/error) #(utils-old/show-popup (i18n/label :t/error)
@ -122,7 +122,7 @@
(when (and (not @input/recording-audio?) (when (and (not @input/recording-audio?)
(nil? (get @input/reviewing-audio-filepath chat-id))) (nil? (get @input/reviewing-audio-filepath chat-id)))
[:<> [:<>
[image-button chat-id] [image-button insets]
[rn/view {:width 12}] [rn/view {:width 12}]
[reactions-button] [reactions-button]
[rn/view {:flex 1}] [rn/view {:flex 1}]

View File

@ -15,8 +15,7 @@
:flex-direction :row :flex-direction :row
:left 0 :left 0
:right 0 :right 0
:margin-top 20 :top 20
:margin-bottom 12
:justify-content :center :justify-content :center
:z-index 1}) :z-index 1})
@ -66,8 +65,8 @@
:height (/ window-width 3) :height (/ window-width 3)
:margin-left (when (not= (mod index 3) 0) 1) :margin-left (when (not= (mod index 3) 0) 1)
:margin-bottom 1 :margin-bottom 1
:border-top-left-radius (when (= index 0) 10) :border-top-left-radius (when (= index 0) 20)
:border-top-right-radius (when (= index 2) 10)}) :border-top-right-radius (when (= index 2) 20)})
(defn overlay (defn overlay
[window-width] [window-width]

View File

@ -1,9 +1,9 @@
(ns status-im2.contexts.chat.photo-selector.view (ns status-im2.contexts.chat.photo-selector.view
(:require (:require
[react-native.gesture :as gesture]
[react-native.platform :as platform] [react-native.platform :as platform]
[status-im2.constants :as constants] [status-im2.constants :as constants]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[react-native.safe-area :as safe-area]
[quo2.components.notifications.info-count :as info-count] [quo2.components.notifications.info-count :as info-count]
[quo2.core :as quo] [quo2.core :as quo]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
@ -13,6 +13,7 @@
[status-im2.contexts.chat.photo-selector.style :as style] [status-im2.contexts.chat.photo-selector.style :as style]
[status-im.utils.core :as utils] [status-im.utils.core :as utils]
[quo.react] [quo.react]
[status-im2.common.bottom-sheet-screen.view :as bottom-sheet-screen]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn on-press-confirm-selection (defn on-press-confirm-selection
@ -80,17 +81,22 @@
(inc (utils/first-index #(= (:uri item) (:uri %)) @selected))])]) (inc (utils/first-index #(= (:uri item) (:uri %)) @selected))])])
(defn album-title (defn album-title
[photos? selected-album selected temporary-selected] [photos? selected-album]
(fn []
[rn/touchable-opacity [rn/touchable-opacity
{:style (style/title-container) {:style (style/title-container)
:active-opacity 1 :active-opacity 1
:accessibility-label :album-title :accessibility-label :album-title
:on-press (fn [] :on-press (fn []
(if photos? ;; TODO: album-selector issue:
(do ;; https://github.com/status-im/status-mobile/issues/15398
(reset! temporary-selected @selected) (js/alert "currently disabled")
(rf/dispatch [:open-modal :album-selector])) ;(if photos?
(rf/dispatch [:navigate-back])))} ; (do
; (reset! temporary-selected @selected)
; (rf/dispatch [:open-modal :album-selector {:insets insets}]))
; (rf/dispatch [:navigate-back]))
)}
[quo/text [quo/text
{:weight :medium {:weight :medium
:ellipsize-mode :tail :ellipsize-mode :tail
@ -99,12 +105,14 @@
selected-album] selected-album]
[rn/view {:style (style/chevron-container)} [rn/view {:style (style/chevron-container)}
[quo/icon (if photos? :i/chevron-down :i/chevron-up) [quo/icon (if photos? :i/chevron-down :i/chevron-up)
{:color (colors/theme-colors colors/neutral-100 colors/white)}]]]) {:color (colors/theme-colors colors/neutral-100 colors/white)}]]]))
(defn photo-selector (defn photo-selector
[] []
[:f> [:f>
(let [temporary-selected (reagent/atom [])] ; used when switching albums (let [{:keys [insets]} (rf/sub [:get-screen-params])
temporary-selected (reagent/atom [])] ; used when switching albums
(fn [] (fn []
(let [selected (reagent/atom []) ; currently selected (let [selected (reagent/atom []) ; currently selected
selected-images (rf/sub [:chats/sending-image]) ; already selected and dispatched selected-images (rf/sub [:chats/sending-image]) ; already selected and dispatched
@ -116,26 +124,19 @@
(reset! selected (vec (vals selected-images))) (reset! selected (vec (vals selected-images)))
(reset! selected @temporary-selected))) (reset! selected @temporary-selected)))
[selected-album]) [selected-album])
[safe-area/consumer [bottom-sheet-screen/view
(fn [insets] (fn [{:keys [scroll-enabled on-scroll]}]
(let [window-width (:width (rn/get-window)) (let [window-width (:width (rn/get-window))
camera-roll-photos (rf/sub [:camera-roll/photos]) camera-roll-photos (rf/sub [:camera-roll/photos])
end-cursor (rf/sub [:camera-roll/end-cursor]) end-cursor (rf/sub [:camera-roll/end-cursor])
loading? (rf/sub [:camera-roll/loading-more]) loading? (rf/sub [:camera-roll/loading-more])
has-next-page? (rf/sub [:camera-roll/has-next-page])] has-next-page? (rf/sub [:camera-roll/has-next-page])]
[rn/view {:style {:flex 1}} [:<>
[rn/view [rn/view
{:style style/buttons-container} {:style style/buttons-container}
(when platform/android? [album-title true selected-album selected temporary-selected insets]
[rn/touchable-opacity
{:active-opacity 1
:on-press #(rf/dispatch [:navigate-back])
:style (style/close-button-container)}
[quo/icon :i/close
{:size 20 :color (colors/theme-colors colors/black colors/white)}]])
[album-title true selected-album selected temporary-selected]
[clear-button selected]] [clear-button selected]]
[rn/flat-list [gesture/flat-list
{:key-fn identity {:key-fn identity
:render-fn image :render-fn image
:render-data {:window-width window-width :selected selected} :render-data {:window-width window-width :selected selected}
@ -143,7 +144,9 @@
:num-columns 3 :num-columns 3
:content-container-style {:width "100%" :content-container-style {:width "100%"
:padding-bottom (+ (:bottom insets) 100) :padding-bottom (+ (:bottom insets) 100)
:padding-top 80} :padding-top 64}
:on-scroll on-scroll
:scroll-enabled scroll-enabled
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor :on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
selected-album loading? selected-album loading?
has-next-page?])}] has-next-page?])}]

View File

@ -43,6 +43,15 @@
:drawBehind true}} :drawBehind true}}
{:statusBar {:style :light}}))) {:statusBar {:style :light}})))
(def bottom-sheet-options
{:topBar {:visible false}
:layout {:componentBackgroundColor :transparent
:backgroundColor :transparent}
:modalPresentationStyle :overCurrentContext})
(def bottom-sheet-insets
{:top false})
(defn screens (defn screens
[] []
(concat (concat
@ -87,7 +96,8 @@
:factor 1.5}}]}}} :factor 1.5}}]}}}
:component lightbox/lightbox} :component lightbox/lightbox}
{:name :photo-selector {:name :photo-selector
:options {:topBar {:visible false}} :insets bottom-sheet-insets
:options bottom-sheet-options
:component photo-selector/photo-selector} :component photo-selector/photo-selector}
{:name :album-selector {:name :album-selector