Lightbox screen animations (#14954)

* lightbox screen animations
This commit is contained in:
Omar Basem 2023-02-02 15:03:59 +04:00 committed by GitHub
parent 2dad2b67e8
commit eed38fe082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 278 additions and 210 deletions

View File

@ -103,6 +103,8 @@
(if (fn? ret) ret js/undefined)) (if (fn? ret) ret js/undefined))
(bean/->js deps)))) (bean/->js deps))))
(def use-callback react/useCallback)
(defn use-effect-once (defn use-effect-once
[effect-fn] [effect-fn]
(use-effect effect-fn)) (use-effect effect-fn))

View File

@ -1,6 +1,8 @@
(ns react-native.gesture (ns react-native.gesture
(:require ["react-native-gesture-handler" :refer (GestureDetector Gesture gestureHandlerRootHOC)] (:require ["react-native-gesture-handler" :refer
[reagent.core :as reagent])) (GestureDetector Gesture gestureHandlerRootHOC TapGestureHandler State)]
[reagent.core :as reagent]
[oops.core :refer [oget]]))
(def gesture-detector (reagent/adapt-react-class GestureDetector)) (def gesture-detector (reagent/adapt-react-class GestureDetector))
(def gesture-handler-root-hoc gestureHandlerRootHOC) (def gesture-handler-root-hoc gestureHandlerRootHOC)
@ -12,3 +14,14 @@
(defn on-start [^js pan handler] (.onStart pan handler)) (defn on-start [^js pan handler] (.onStart pan handler))
(defn on-end [^js pan handler] (.onEnd pan handler)) (defn on-end [^js pan handler] (.onEnd pan handler))
(def tap-gesture-handler
(reagent/adapt-react-class TapGestureHandler))
(def states
{:began (oget State "BEGAN")
:active (oget State "ACTIVE")
:cancelled (oget State "CANCELLED")
:end (oget State "END")
:failed (oget State "FAILED")
:undetermined (oget State "UNDETERMINED")})

View File

@ -1,20 +1,23 @@
(ns status-im2.contexts.chat.lightbox.style (ns status-im2.contexts.chat.lightbox.style
(:require [quo2.foundations.colors :as colors])) (:require [quo2.foundations.colors :as colors]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]))
(defn container-view (def container-view
[padding-top]
{:background-color :black {:background-color :black
:height "100%" :height "100%"})
:padding-top padding-top})
(defn top-view-container (defn top-view-container
[top-inset] [top-inset opacity]
{:position :absolute (reanimated/apply-animations-to-style
:left 20 {:opacity opacity}
:top (+ 12 top-inset) {:position :absolute
:z-index 1 :left 20
:flex-direction :row :top (if platform/ios? (+ 12 top-inset) 12)
:width "100%"}) :z-index 4
:flex-direction :row
:width "100%"}))
(def close-container (def close-container
{:width 32 {:width 32
@ -26,15 +29,18 @@
(def top-right-buttons (def top-right-buttons
{:position :absolute {:position :absolute
:right 20 :right 40
:flex-direction :row}) :flex-direction :row})
(defn gradient-container (defn gradient-container
[insets] [insets opacity]
{:width "100%" (reanimated/apply-animations-to-style
;:height (+ (:bottom insets) 65) {:opacity opacity}
:position :absolute {:width "100%"
:bottom (:bottom insets)}) :position :absolute
:bottom 0
:padding-bottom (:bottom insets)
:z-index 3}))
(def text-style (def text-style
{:color colors/white {:color colors/white

View File

@ -1,139 +1,191 @@
(ns status-im2.contexts.chat.lightbox.view (ns status-im2.contexts.chat.lightbox.view
(:require [quo2.core :as quo] (:require
[quo2.foundations.colors :as colors] [quo2.core :as quo]
[react-native.core :as rn] [quo2.foundations.colors :as colors]
[react-native.fast-image :as fast-image] [react-native.core :as rn]
[utils.re-frame :as rf] [react-native.fast-image :as fast-image]
[react-native.safe-area :as safe-area] [react-native.reanimated :as reanimated]
[reagent.core :as reagent] [utils.re-frame :as rf]
[oops.core :as oops] [react-native.safe-area :as safe-area]
[status-im2.contexts.chat.lightbox.style :as style] [reagent.core :as reagent]
[utils.datetime :as datetime] [status-im2.contexts.chat.lightbox.style :as style]
[react-native.linear-gradient :as linear-gradient])) [utils.datetime :as datetime]
[react-native.gesture :as gesture]
[oops.core :refer [oget]]))
(def flat-list-ref (atom nil)) (def flat-list-ref (atom nil))
(def small-list-ref (atom nil)) (def small-list-ref (atom nil))
(def small-image-size 40) (def small-image-size 40)
(def focused-image-size 56)
(defn toggle-opacity
[opacity-value border-value transparent?]
(let [opacity (reanimated/get-shared-value opacity-value)]
(reanimated/set-shared-value opacity-value (reanimated/with-timing (if (= opacity 1) 0 1)))
(reanimated/set-shared-value border-value (reanimated/with-timing (if (= opacity 1) 0 12)))
(reset! transparent? (not @transparent?))))
(defn image (defn image
[message] [message opacity-value border-value transparent?]
(let [shared-element-id (rf/sub [:shared-element-id]) [:f>
window-width (:width (rn/get-window)) (fn []
height (* (or (:image-height message) 1000) (let [shared-element-id (rf/sub [:shared-element-id])
(/ window-width (or (:image-width message) 1000)))] width (:width (rn/get-window))
[fast-image/fast-image height (* (or (:image-height message) 1000)
{:source {:uri (:image (:content message))} (/ width (or (:image-width message) 1000)))]
:style {:width window-width [gesture/tap-gesture-handler
:height height {:on-handler-state-change (fn [e]
:border-radius 12} (when (= (oget e "nativeEvent.state") (:active gesture/states))
:native-ID (when (= shared-element-id (:message-id message)) :shared-element)}])) (toggle-opacity opacity-value border-value transparent?)))}
[rn/view {:style {:flex-direction :row}}
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:border-radius border-value}
{:overflow :hidden})}
[fast-image/fast-image
{:source {:uri (:image (:content message))}
:style {:width width
:height height}
:native-ID (when (= shared-element-id (:message-id message)) :shared-element)}]]
[rn/view {:style {:width 16}}]]]))])
(defn get-item-layout (defn get-item-layout
[_ index] [_ index]
(let [window-width (:width (rn/get-window))] (let [window-width (:width (rn/get-window))]
#js {:length window-width :offset (* window-width index) :index index})) #js {:length window-width :offset (* (+ window-width 16) index) :index index}))
(defn get-small-item-layout (defn get-small-item-layout
[_ index] [_ index]
#js {:length small-image-size :offset (* (+ small-image-size 8) index) :index index}) #js {:length small-image-size :offset (* (+ small-image-size 8) index) :index index})
(defn on-viewable-items-changed (defn on-viewable-items-changed
[e] [e scroll-index]
(let [changed (-> e (oops/oget :changed) first)] (let [changed (-> e (oget :changed) first)
(.scrollToIndex ^js @small-list-ref #js {:animated true :index (oops/oget changed :index)}) index (oget changed :index)]
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oops/oget changed :item))]))) (reset! scroll-index index)
(.scrollToIndex ^js @small-list-ref #js {:animated true :index index})
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oget changed :item))])))
(defn top-view (defn top-view
[{:keys [from timestamp]} insets] [{:keys [from timestamp]} insets opacity-value transparent?]
(let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from]))] [:f>
[rn/view (fn []
{:style (style/top-view-container (:top insets))} (let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from]))]
[rn/touchable-opacity [reanimated/view
{:active-opacity 1 {:style (style/top-view-container (:top insets) opacity-value)}
:on-press #(rf/dispatch [:navigate-back]) [rn/touchable-opacity
:style style/close-container} {:on-press #(when-not @transparent? (rf/dispatch [:navigate-back]))
[quo/icon :close {:size 20 :color colors/white}]] :style style/close-container}
[rn/view {:style {:margin-left 12}} [quo/icon :close {:size 20 :color colors/white}]]
[quo/text [rn/view {:style {:margin-left 12}}
{:weight :semi-bold [quo/text
:size :paragraph-1 {:weight :semi-bold
:style {:color colors/white}} display-name] :size :paragraph-1
[quo/text :style {:color colors/white}} display-name]
{:weight :medium [quo/text
:size :paragraph-2 {:weight :medium
:style {:color colors/neutral-40}} (datetime/to-short-str timestamp)]] :size :paragraph-2
[rn/view {:style style/top-right-buttons} :style {:color colors/neutral-40}} (datetime/to-short-str timestamp)]]
[rn/touchable-opacity [rn/view {:style style/top-right-buttons}
{:active-opacity 1 [rn/touchable-opacity
:on-press #(js/alert "to be implemented") {:active-opacity 1
:style (merge style/close-container {:margin-right 12})} :on-press #(js/alert "to be implemented")
[quo/icon :share {:size 20 :color colors/white}]] :style (merge style/close-container {:margin-right 12})}
[rn/touchable-opacity [quo/icon :share {:size 20 :color colors/white}]]
{:active-opacity 1 [rn/touchable-opacity
:on-press #(js/alert "to be implemented") {:active-opacity 1
:style style/close-container} :on-press #(js/alert "to be implemented")
[quo/icon :options {:size 20 :color colors/white}]]]])) :style style/close-container}
[quo/icon :options {:size 20 :color colors/white}]]]]))])
(defn small-image (defn small-image
[item] [item index scroll-index]
[fast-image/fast-image [:f>
{:source {:uri (:image (:content item))} (fn []
:style {:width small-image-size (let [size (if (= @scroll-index index) focused-image-size small-image-size)
:height small-image-size size-value (reanimated/use-shared-value size)]
:border-radius 10}}]) (reanimated/set-shared-value size-value (reanimated/with-timing size))
[rn/touchable-opacity
{:active-opacity 1
:on-press (fn []
(reset! scroll-index index)
(.scrollToIndex ^js @small-list-ref #js {:animated true :index index})
(.scrollToIndex ^js @flat-list-ref #js {:animated true :index index}))}
[reanimated/fast-image
{:source {:uri (:image (:content item))}
:style (reanimated/apply-animations-to-style {:width size-value
:height size-value}
{:border-radius 10})}]]))])
(defn bottom-view (defn bottom-view
[messages insets] [messages index scroll-index insets opacity-value]
(let [text (get-in (first messages) [:content :text]) [:f>
padding-horizontal (- (/ (:width (rn/get-window)) 2) (/ small-image-size 2))] (fn []
[linear-gradient/linear-gradient (let [text (get-in (first messages) [:content :text])
{:colors [:black :transparent] padding-horizontal (- (/ (:width (rn/get-window)) 2) (/ focused-image-size 2))]
:start {:x 0 :y 1} [reanimated/linear-gradient
:end {:x 0 :y 0} {:colors [:black :transparent]
:style (style/gradient-container insets)} :start {:x 0 :y 1}
[rn/text :end {:x 0 :y 0}
{:style style/text-style} text] :style (style/gradient-container insets opacity-value)}
[rn/flat-list [rn/text
{:ref #(reset! small-list-ref %) {:style style/text-style} text]
:key-fn :message-id [rn/flat-list
:data messages {:ref #(reset! small-list-ref %)
:render-fn small-image :key-fn :message-id
:horizontal true :style {:height 68}
:get-item-layout get-small-item-layout :data messages
:separator [rn/view {:style {:width 8}}] :render-fn (fn [item index] [small-image item index scroll-index])
:content-container-style {:padding-vertical 12 :horizontal true
:padding-horizontal padding-horizontal}}]])) :get-item-layout get-small-item-layout
:separator [rn/view {:style {:width 8}}]
:initial-scroll-index index
:content-container-style {:padding-vertical 12
:padding-horizontal padding-horizontal
:align-items :center
:justify-content :center}}]]))])
(defn lightbox (defn lightbox
[] []
(let [{:keys [messages index]} (rf/sub [:get-screen-params]) [:f>
;; The initial value of data is the image that was pressed (and not the whole album) in order for (fn []
;; the transition animation to execute properly, otherwise it would animate towards outside the (let [{:keys [messages index]} (rf/sub [:get-screen-params])
;; screen (even if we have `initialScrollIndex` set). ;; The initial value of data is the image that was pressed (and not the whole album) in order
data (reagent/atom [(nth messages index)])] ;; for the transition animation to execute properly, otherwise it would animate towards
(reset! data messages) ;; outside the screen (even if we have `initialScrollIndex` set).
;; We use setTimeout to enqueue `scrollToIndex` until the `data` has been updated. data (reagent/atom [(nth messages index)])
(js/setTimeout #(do scroll-index (reagent/atom index)
(.scrollToIndex ^js @flat-list-ref #js {:animated false :index index}) transparent? (reagent/atom false)
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index})) opacity-value (reanimated/use-shared-value 1)
0) border-value (reanimated/use-shared-value 12)
[safe-area/consumer window-width (:width (rn/get-window))]
(fn [insets] (reset! data messages)
[rn/view [safe-area/consumer
{:style (style/container-view (:top insets))} (fn [insets]
[top-view (first messages) insets] [:f>
[rn/flat-list (fn []
{:ref #(reset! flat-list-ref %) ;; We use setTimeout to enqueue `scrollToIndex` until the `data` has been updated.
:key-fn :message-id (js/setTimeout #(.scrollToIndex ^js @flat-list-ref #js {:animated false :index index}) 0)
:data @data [rn/view {:style style/container-view}
:render-fn image [top-view (first messages) insets opacity-value transparent?]
:horizontal true [rn/flat-list
:paging-enabled true {:ref #(reset! flat-list-ref %)
:get-item-layout get-item-layout :key-fn :message-id
:viewability-config {:view-area-coverage-percent-threshold 50} :style {:width (+ window-width 16)}
:on-viewable-items-changed on-viewable-items-changed :data @data
:content-container-style {:justify-content :center :render-fn (fn [item] [image item opacity-value border-value
:align-items :center}}] transparent?])
[bottom-view messages insets]])])) :horizontal true
:paging-enabled true
:get-item-layout get-item-layout
:viewability-config {:view-area-coverage-percent-threshold 50}
:on-viewable-items-changed (rn/use-callback (fn [e]
(on-viewable-items-changed e
scroll-index)))
:content-container-style {:justify-content :center
:align-items :center}}]
[bottom-view messages index scroll-index insets opacity-value]])])]))])

View File

@ -10,31 +10,6 @@
(def rectangular-style-count 3) (def rectangular-style-count 3)
(defn border-tlr
[index]
(when (= index 0) 12))
(defn border-trr
[index count album-style]
(when (or (and (= index 1) (not= count rectangular-style-count))
(and (= index 0) (= count rectangular-style-count) (= album-style :landscape))
(and (= index 1) (= count rectangular-style-count) (= album-style :portrait)))
12))
(defn border-blr
[index count album-style]
(when (or (and (= index 0) (< count rectangular-style-count))
(and (= index 2) (> count rectangular-style-count))
(and (= index 1) (= count rectangular-style-count) (= album-style :landscape))
(and (= index 0) (= count rectangular-style-count) (= album-style :portrait)))
12))
(defn border-brr
[index count]
(when (or (and (= index 1) (< count rectangular-style-count))
(and (= index (- (min count constants/max-album-photos) 1)) (> count 2)))
12))
(defn find-size (defn find-size
[size-arr album-style] [size-arr album-style]
(if (= album-style :landscape) (if (= album-style :landscape)
@ -79,8 +54,7 @@
(when (and (> images-count constants/max-album-photos) (when (and (> images-count constants/max-album-photos)
(= index (- constants/max-album-photos 1))) (= index (- constants/max-album-photos 1)))
[rn/view [rn/view
{:style (merge style/overlay {:style style/overlay}
{:border-bottom-right-radius (border-brr index images-count)})}
[quo/text [quo/text
{:weight :bold {:weight :bold
:size :heading-2 :size :heading-2

View File

@ -21,7 +21,12 @@
{:source {:uri uri} {:source {:uri uri}
:style style/cover}] :style style/cover}]
[rn/view {:style {:margin-left 12}} [rn/view {:style {:margin-left 12}}
[quo/text {:weight :medium} title] [quo/text
{:weight :medium
:ellipsize-mode :tail
:number-of-lines 1
:style {:margin-right 50}}
title]
[quo/text [quo/text
{:size :paragraph-2 {:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}} :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}}
@ -40,6 +45,10 @@
{:label title {:label title
:container-style style/divider}])) :container-style style/divider}]))
(defn key-fn
[item index]
(str (:title item) index))
(defn album-selector (defn album-selector
[] []
[:f> [:f>
@ -61,4 +70,4 @@
:render-section-header-fn section-header :render-section-header-fn section-header
:style {:margin-top 12} :style {:margin-top 12}
:content-container-style {:padding-bottom 40} :content-container-style {:padding-bottom 40}
:key-fn :title}]]))]) :key-fn key-fn}]]))])

View File

@ -29,8 +29,7 @@
:position :absolute :position :absolute
:right 20}) :right 20})
(defn camera-button-container (def camera-button-container
[]
{:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80) {:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80)
:width 32 :width 32
:height 32 :height 32
@ -82,4 +81,3 @@
:border-radius 8 :border-radius 8
:top 8 :top 8
:right 8}) :right 8})

View File

@ -80,15 +80,23 @@
(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] [photos? selected-album selected temporary-selected]
[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 #(rf/dispatch (if photos? :on-press (fn []
[:open-modal :album-selector] (if photos?
[:navigate-back]))} (do
[quo/text {:weight :medium} selected-album] (reset! temporary-selected @selected)
(rf/dispatch [:open-modal :album-selector]))
(rf/dispatch [:navigate-back])))}
[quo/text
{:weight :medium
:ellipsize-mode :tail
:number-of-lines 1
:style {:max-width 150}}
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)}]]])
@ -96,41 +104,47 @@
(defn photo-selector (defn photo-selector
[] []
[:f> [:f>
(fn [] (let [temporary-selected (reagent/atom [])] ; used when switching albums
(let [selected-images (rf/sub [:chats/sending-image]) (fn []
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent)) (let [selected (reagent/atom []) ; currently selected
selected (reagent/atom [])] selected-images (rf/sub [:chats/sending-image]) ; already selected and dispatched
(rn/use-effect selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
(fn [] (rn/use-effect
(rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album]) (fn []
(if selected-images (rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album])
(reset! selected (vec (vals selected-images))) (if (seq selected-images)
(reset! selected []))) (reset! selected (vec (vals selected-images)))
[selected-album]) (reset! selected @temporary-selected)))
[safe-area/consumer [selected-album])
(fn [insets] [safe-area/consumer
(let [window-width (:width (rn/get-window)) (fn [insets]
camera-roll-photos (rf/sub [:camera-roll/photos]) (let [window-width (:width (rn/get-window))
end-cursor (rf/sub [:camera-roll/end-cursor]) camera-roll-photos (rf/sub [:camera-roll/photos])
loading? (rf/sub [:camera-roll/loading-more]) end-cursor (rf/sub [:camera-roll/end-cursor])
has-next-page? (rf/sub [:camera-roll/has-next-page])] loading? (rf/sub [:camera-roll/loading-more])
[rn/view {:style {:flex 1}} has-next-page? (rf/sub [:camera-roll/has-next-page])]
[rn/view [rn/view {:style {:flex 1}}
{:style style/buttons-container} [rn/view
[album-title true selected-album] {:style style/buttons-container}
[clear-button selected]] (when platform/android?
[rn/flat-list [rn/touchable-opacity
{:key-fn identity {:active-opacity 1
:render-fn image :on-press #(rf/dispatch [:navigate-back])
:render-data {:window-width window-width :selected selected} :style style/camera-button-container}
:data camera-roll-photos [quo/icon :i/close
:num-columns 3 {:size 20 :color (colors/theme-colors colors/black colors/white)}]])
:content-container-style {:width "100%" [album-title true selected-album selected temporary-selected]
:padding-bottom (+ (:bottom insets) 100) [clear-button selected]]
:padding-top 80} [rn/flat-list
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor {:key-fn identity
selected-album loading? :render-fn image
has-next-page?])}] :render-data {:window-width window-width :selected selected}
[bottom-gradient selected-images insets selected]]))]))]) :data camera-roll-photos
:num-columns 3
:content-container-style {:width "100%"
:padding-bottom (+ (:bottom insets) 100)
:padding-top 80}
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
selected-album loading?
has-next-page?])}]
[bottom-gradient selected-images insets selected]]))])))])

View File

@ -3,7 +3,7 @@
"_comment": "Instead use: scripts/update-status-go.sh <rev>", "_comment": "Instead use: scripts/update-status-go.sh <rev>",
"owner": "status-im", "owner": "status-im",
"repo": "status-go", "repo": "status-go",
"version": "v0.126.1", "version": "v0.128.0",
"commit-sha1": "cb624b0cc14d95e46754210c9153f8a0444d2845", "commit-sha1": "82596b2b67f82ae6329ad88e34be6240d08c8ff1",
"src-sha256": "1wrjlzwqj7qzsvh2aba4fjfp29ckx416lkxsk3c7las88ji93wzn" "src-sha256": "17n461i2cglvs66rpqxdxpka4k9ld9ds2ha6scdli2nrfg9jb4q3"
} }