feat: drag to dismiss lightbox (#15349)

* feat: drag to dismiss lightbox
This commit is contained in:
Omar Basem 2023-03-15 16:37:01 +04:00 committed by GitHub
parent 8546727f84
commit f640eb8c8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 447 additions and 297 deletions

View File

@ -0,0 +1,10 @@
import { useDerivedValue } from 'react-native-reanimated';
export function infoLayout(input, isTop) {
return useDerivedValue(
function () {
'worklet'
return isTop ? input.value : -input.value
}
);
}

View File

@ -407,6 +407,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
"../src/js/worklets/bottom_sheet.js" #js {} "../src/js/worklets/bottom_sheet.js" #js {}
"../src/js/worklets/record_audio.js" #js {} "../src/js/worklets/record_audio.js" #js {}
"../src/js/worklets/scroll_view.js" #js {} "../src/js/worklets/scroll_view.js" #js {}
"../src/js/worklets/lightbox.js" #js {}
"./fleets.js" default-fleets "./fleets.js" default-fleets
"@walletconnect/client" wallet-connect-client "@walletconnect/client" wallet-connect-client
"../translations/ar.json" (js/JSON.parse (slurp "./translations/ar.json")) "../translations/ar.json" (js/JSON.parse (slurp "./translations/ar.json"))

View File

@ -5,7 +5,9 @@
RectButton RectButton
Swipeable Swipeable
TouchableWithoutFeedback TouchableWithoutFeedback
gestureHandlerRootHOC)] gestureHandlerRootHOC
FlatList)]
[react-native.flat-list :as rn-flat-list]
[reagent.core :as reagent])) [reagent.core :as reagent]))
(def gesture-detector (reagent/adapt-react-class GestureDetector)) (def gesture-detector (reagent/adapt-react-class GestureDetector))
@ -28,6 +30,8 @@
(defn on-finalize [gesture handler] (.onFinalize ^js gesture handler)) (defn on-finalize [gesture handler] (.onFinalize ^js gesture handler))
(defn max-pointers [gesture count] (.maxPointers ^js gesture count))
(defn number-of-taps [gesture count] (.numberOfTaps ^js gesture count)) (defn number-of-taps [gesture count] (.numberOfTaps ^js gesture count))
(defn enabled [gesture enabled?] (.enabled ^js gesture enabled?)) (defn enabled [gesture enabled?] (.enabled ^js gesture enabled?))
@ -63,3 +67,8 @@
(fn [& args] (fn [& args]
(reagent/as-element (apply render-right-actions args)))))] (reagent/as-element (apply render-right-actions args)))))]
children)) children))
(def gesture-flat-list (reagent/adapt-react-class FlatList))
(defn flat-list
[props]
[gesture-flat-list (rn-flat-list/base-list-props props)])

View File

@ -5,26 +5,21 @@
[react-native.reanimated :as reanimated] [react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.style :as style] [status-im2.contexts.chat.lightbox.style :as style]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[status-im2.contexts.chat.lightbox.animations :as anim])) [status-im2.contexts.chat.lightbox.animations :as anim]
[status-im2.contexts.chat.lightbox.constants :as c]))
(def small-image-size 40)
(def focused-image-size 56)
(def small-list-height 80)
(defn get-small-item-layout (defn get-small-item-layout
[_ index] [_ index]
#js #js
{:length small-image-size {:length c/small-image-size
:offset (* (+ small-image-size 8) index) :offset (* (+ c/small-image-size 8) index)
:index index}) :index index})
(defn small-image (defn small-image
[item index _ {:keys [scroll-index atoms]}] [item index _ {:keys [scroll-index atoms]}]
[:f> [:f>
(fn [] (fn []
(let [size (if (= @scroll-index index) focused-image-size small-image-size) (let [size (if (= @scroll-index index) c/focused-image-size c/small-image-size)
size-value (anim/use-val size) size-value (anim/use-val size)
{:keys [scroll-index-lock? small-list-ref {:keys [scroll-index-lock? small-list-ref
flat-list-ref]} atoms] flat-list-ref]} atoms]
@ -51,22 +46,22 @@
{:border-radius 10})}]]))]) {:border-radius 10})}]]))])
(defn bottom-view (defn bottom-view
[messages index scroll-index insets animations item-width atoms] [messages index scroll-index insets animations derived item-width atoms]
[:f> [:f>
(fn [] (fn []
(let [text (get-in (first messages) [:content :text]) (let [text (get-in (first messages) [:content :text])
padding-horizontal (- (/ item-width 2) (/ focused-image-size 2))] padding-horizontal (- (/ item-width 2) (/ c/focused-image-size 2))]
[reanimated/linear-gradient [reanimated/linear-gradient
{:colors [:black :transparent] {:colors [:black :transparent]
:start {:x 0 :y 1} :start {:x 0 :y 1}
:end {:x 0 :y 0} :end {:x 0 :y 0}
:style (style/gradient-container insets animations)} :style (style/gradient-container insets animations derived)}
(when (not= text "placeholder") (when (not= text "placeholder")
[rn/text {:style style/text-style} text]) [rn/text {:style style/text-style} text])
[rn/flat-list [rn/flat-list
{:ref #(reset! (:small-list-ref atoms) %) {:ref #(reset! (:small-list-ref atoms) %)
:key-fn :message-id :key-fn :message-id
:style {:height small-list-height} :style {:height c/small-list-height}
:data messages :data messages
:render-fn small-image :render-fn small-image
:render-data {:scroll-index scroll-index :render-data {:scroll-index scroll-index

View File

@ -0,0 +1,13 @@
(ns status-im2.contexts.chat.lightbox.constants)
(def ^:const small-image-size 40)
(def ^:const focused-extra-size 16)
(def ^:const focused-image-size (+ small-image-size focused-extra-size))
(def ^:const small-list-height 80)
(def ^:const small-list-padding-vertical 12)
(def ^:const top-view-height 56)

View File

@ -1,13 +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.platform :as platform]
[react-native.reanimated :as reanimated])) [react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.constants :as c]))
;;;; VIEW
(defn image
[width height]
{:flex-direction :row
:width width
:height height
:align-items :center
:justify-content :center})
;;;; TOP-VIEW ;;;; TOP-VIEW
(defn top-view-container (defn top-view-container
[top-inset {:keys [opacity rotate top-view-y top-view-x top-view-width top-view-bg top-layout]} [top-inset window-width bg-color landscape?
window-width {:keys [opacity rotate top-view-y top-view-x top-view-width top-view-bg]}
bg-color] {:keys [top-layout]}]
(reanimated/apply-animations-to-style (reanimated/apply-animations-to-style
(if platform/ios? (if platform/ios?
{:transform [{:translateY top-layout} {:transform [{:translateY top-layout}
@ -21,9 +31,8 @@
:opacity opacity}) :opacity opacity})
{:position :absolute {:position :absolute
:padding-horizontal 20 :padding-horizontal 20
:top (if platform/ios? top-inset 0) :top (if (or platform/ios? (not landscape?)) top-inset 0)
;; height defined in top_view.cljs, but can't import due to circular dependency :height c/top-view-height
:height 56
:z-index 4 :z-index 4
:flex-direction :row :flex-direction :row
:justify-content :space-between :justify-content :space-between
@ -31,6 +40,14 @@
:background-color (when platform/android? bg-color) :background-color (when platform/android? bg-color)
:align-items :center})) :align-items :center}))
(defn top-gradient
[insets]
{:position :absolute
:height (+ c/top-view-height (:top insets) 0)
:top (- (:top insets))
:left 0
:right 0})
(def close-container (def close-container
{:width 32 {:width 32
:height 32 :height 32
@ -44,18 +61,20 @@
;;;; BOTTOM-VIEW ;;;; BOTTOM-VIEW
(defn gradient-container (defn gradient-container
[insets {:keys [opacity bottom-layout]}] [insets {:keys [opacity]} {:keys [bottom-layout]}]
(reanimated/apply-animations-to-style (reanimated/apply-animations-to-style
{:transform [{:translateY bottom-layout}] {:transform [{:translateY bottom-layout}]
:opacity opacity} :opacity opacity}
{:position :absolute {:position :absolute
:bottom 0 :bottom 0
:padding-bottom (:bottom insets) :padding-bottom (if platform/ios?
(:bottom insets)
(+ (:bottom insets) c/small-list-padding-vertical c/focused-extra-size))
:z-index 3})) :z-index 3}))
(defn content-container (defn content-container
[padding-horizontal] [padding-horizontal]
{:padding-vertical 12 {:padding-vertical c/small-list-padding-vertical
:padding-horizontal padding-horizontal :padding-horizontal padding-horizontal
:align-items :center :align-items :center
:justify-content :center}) :justify-content :center})

View File

@ -9,14 +9,13 @@
[status-im2.contexts.chat.lightbox.animations :as anim] [status-im2.contexts.chat.lightbox.animations :as anim]
[status-im2.contexts.chat.lightbox.style :as style] [status-im2.contexts.chat.lightbox.style :as style]
[utils.datetime :as datetime] [utils.datetime :as datetime]
[utils.re-frame :as rf])) [utils.re-frame :as rf]
[status-im2.contexts.chat.lightbox.constants :as c]))
(def ^:const top-view-height 56)
(defn animate-rotation (defn animate-rotation
[result screen-width screen-height insets-atom [result screen-width screen-height insets
{:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}] {:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}]
(let [top-x (+ (/ top-view-height 2) (:top insets-atom))] (let [top-x (+ (/ c/top-view-height 2) (:top insets))]
(cond (cond
(= result orientation/landscape-left) (= result orientation/landscape-left)
(do (do
@ -41,22 +40,30 @@
(anim/animate top-view-bg colors/neutral-100-opa-0))))) (anim/animate top-view-bg colors/neutral-100-opa-0)))))
(defn top-view (defn top-view
[{:keys [from timestamp]} insets index animations landscape? screen-width] [{:keys [from timestamp]} insets index animations derived landscape? screen-width]
[:f> [:f>
(fn [] (fn []
(let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from])) (let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity
bg-color (if landscape? colors/neutral-100-opa-70 colors/neutral-100-opa-0)] from]))
bg-color (if landscape?
colors/neutral-100-opa-70
colors/neutral-100-opa-0)
{:keys [background-color opacity]} animations]
[reanimated/view [reanimated/view
{:style (style/top-view-container (:top insets) animations screen-width bg-color)} {:style
(style/top-view-container (:top insets) screen-width bg-color landscape? animations derived)}
[reanimated/linear-gradient
{:colors [(colors/alpha "#000000" 0.8) :transparent]
:start {:x 0 :y 0}
:end {:x 0 :y 1}
:style (style/top-gradient insets)}]
[rn/view [rn/view
{:style {:flex-direction :row {:style {:flex-direction :row
:align-items :center}} :align-items :center}}
[rn/touchable-opacity [rn/touchable-opacity
{:on-press (fn [] {:on-press (fn []
(when platform/ios? (anim/animate background-color :transparent)
(anim/animate (:background-color animations) (anim/animate opacity 0)
(reanimated/with-timing "rgba(0,0,0,0)")))
(anim/animate (:opacity animations) 0)
(rf/dispatch (if platform/ios? (rf/dispatch (if platform/ios?
[:chat.ui/exit-lightbox-signal @index] [:chat.ui/exit-lightbox-signal @index]
[:navigate-back]))) [:navigate-back])))

View File

@ -3,25 +3,34 @@
[clojure.string :as string] [clojure.string :as string]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.navigation :as navigation]
[react-native.orientation :as orientation] [react-native.orientation :as orientation]
[react-native.platform :as platform] [react-native.platform :as platform]
[react-native.reanimated :as reanimated] [react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.animations :as anim] [status-im2.contexts.chat.lightbox.animations :as anim]
[status-im2.contexts.chat.lightbox.style :as style]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent] [reagent.core :as reagent]
[react-native.gesture :as gesture]
[status-im2.contexts.chat.lightbox.zoomable-image.view :as zoomable-image] [status-im2.contexts.chat.lightbox.zoomable-image.view :as zoomable-image]
[status-im2.contexts.chat.lightbox.top-view :as top-view] [status-im2.contexts.chat.lightbox.top-view :as top-view]
[status-im2.contexts.chat.lightbox.bottom-view :as bottom-view] [status-im2.contexts.chat.lightbox.bottom-view :as bottom-view]
[utils.worklets.lightbox :as worklet]
[oops.core :refer [oget]])) [oops.core :refer [oget]]))
(def seperator-width 16) (def ^:const seperator-width 16)
(def ^:const drag-threshold 100)
(defn toggle-opacity (defn toggle-opacity
[opacity-value border-value transparent? index {:keys [small-list-ref]}] [index {:keys [opacity-value border-value transparent? atoms]} portrait?]
(let [opacity (reanimated/get-shared-value opacity-value)] (let [{:keys [small-list-ref]} atoms
opacity (reanimated/get-shared-value opacity-value)]
(if (= opacity 1) (if (= opacity 1)
(do (do
(when platform/ios?
;; status-bar issue: https://github.com/status-im/status-mobile/issues/15343
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}}) 75))
(anim/animate opacity-value 0) (anim/animate opacity-value 0)
(js/setTimeout #(reset! transparent? (not @transparent?)) 400)) (js/setTimeout #(reset! transparent? (not @transparent?)) 400))
(do (do
@ -29,17 +38,19 @@
(js/setTimeout #(anim/animate opacity-value 1) 50) (js/setTimeout #(anim/animate opacity-value 1) 50)
(js/setTimeout #(when @small-list-ref (js/setTimeout #(when @small-list-ref
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index})) (.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
100))) 100)
(when (and platform/ios? portrait?)
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}}) 150))))
(anim/animate border-value (if (= opacity 1) 0 12)))) (anim/animate border-value (if (= opacity 1) 0 12))))
(defn handle-orientation (defn handle-orientation
[result index window animations {:keys [flat-list-ref insets-atom]}] [result index window-width window-height animations insets {:keys [flat-list-ref]}]
(let [screen-width (if (or platform/ios? (= result orientation/portrait)) (let [screen-width (if (or platform/ios? (= result orientation/portrait))
(:width window) window-width
(:height window)) window-height)
screen-height (if (or platform/ios? (= result orientation/portrait)) screen-height (if (or platform/ios? (= result orientation/portrait))
(:height window) window-height
(:width window)) window-width)
landscape? (string/includes? result orientation/landscape) landscape? (string/includes? result orientation/landscape)
item-width (if (and landscape? platform/ios?) screen-height screen-width) item-width (if (and landscape? platform/ios?) screen-height screen-width)
timeout (if platform/ios? 50 100)] timeout (if platform/ios? 50 100)]
@ -56,7 +67,7 @@
#js {:animated false :offset (* (+ item-width seperator-width) @index)})) #js {:animated false :offset (* (+ item-width seperator-width) @index)}))
timeout) timeout)
(when platform/ios? (when platform/ios?
(top-view/animate-rotation result screen-width screen-height @insets-atom animations)))) (top-view/animate-rotation result screen-width screen-height insets animations))))
(defn get-item-layout (defn get-item-layout
[_ index item-width] [_ index item-width]
@ -73,77 +84,104 @@
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oget changed :item))])))) (rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oget changed :item))]))))
(defn image (defn image
[message index _ {:keys [opacity-value border-value transparent? width height atoms]}] [message index _ {:keys [screen-width screen-height] :as args}]
[:f>
(fn []
[rn/view [rn/view
{:style {:flex-direction :row {:style (style/image (+ screen-width seperator-width) screen-height)}
:width (+ width seperator-width) [zoomable-image/zoomable-image message index args
:height height #(toggle-opacity index args %)]
:align-items :center [rn/view {:style {:width seperator-width}}]])
:justify-content :center}}
[zoomable-image/zoomable-image message index border-value
#(toggle-opacity opacity-value border-value transparent? index atoms)]
[rn/view {:style {:width seperator-width}}]])])
;; using `safe-area/consumer` in this component in iOS causes unnecessary re-renders and weird behaviour (defn drag-gesture
;; using `use-safe-area` on Android crashes the app with error `rendered fewer hooks than expected` [{:keys [pan-x pan-y background-color opacity layout]} x? set-full-height?]
(defn container-view (->
[children] (gesture/gesture-pan)
(if platform/ios? (gesture/enabled true)
[:f> children] (gesture/max-pointers 1)
[safe-area/consumer children])) (gesture/on-start #(reset! set-full-height? false))
(gesture/on-update (fn [e]
(let [translation (if x? (oget e "translationX") (oget e "translationY"))
progress (Math/abs (/ translation drag-threshold))]
(anim/set-val (if x? pan-x pan-y) translation)
(anim/set-val opacity (- 1 progress))
(anim/set-val layout (* progress -20)))))
(gesture/on-end (fn [e]
(if (> (Math/abs (if x? (oget e "translationX") (oget e "translationY")))
drag-threshold)
(do
(anim/animate background-color "rgba(0,0,0,0)")
(anim/animate opacity 0)
(rf/dispatch [:navigate-back]))
(do
#(reset! set-full-height? true)
(anim/animate (if x? pan-x pan-y) 0)
(anim/animate opacity 1)
(anim/animate layout 0)))))))
(defn lightbox (defn lightbox
[] []
[:f> [:f>
(fn [] (fn []
(let [{:keys [messages index]} (rf/sub [:get-screen-params]) ;; we get `insets` from `screen-params` because trying to consume it from
;; lightbox screen causes lots of problems
(let [{:keys [messages index insets]} (rf/sub [:get-screen-params])
atoms {:flat-list-ref (atom nil) atoms {:flat-list-ref (atom nil)
:small-list-ref (atom nil) :small-list-ref (atom nil)
:scroll-index-lock? (atom true) :scroll-index-lock? (atom true)}
:insets-atom (atom nil)}
;; The initial value of data is the image that was pressed (and not the whole album) in order ;; The initial value of data is the image that was pressed (and not the whole album) in order
;; for the transition animation to execute properly, otherwise it would animate towards ;; for the transition animation to execute properly, otherwise it would animate towards
;; outside the screen (even if we have `initialScrollIndex` set). ;; outside the screen (even if we have `initialScrollIndex` set).
data (reagent/atom [(nth messages index)]) data (reagent/atom [(nth messages index)])
scroll-index (reagent/atom index) scroll-index (reagent/atom index)
transparent? (reagent/atom false) transparent? (reagent/atom false)
set-full-height? (reagent/atom false)
window (rf/sub [:dimensions/window]) window (rf/sub [:dimensions/window])
window-width (:width window)
window-height (:height window)
window-height (if platform/android?
(+ window-height (:top insets))
window-height)
animations {:background-color (anim/use-val "rgba(0,0,0,0)") animations {:background-color (anim/use-val "rgba(0,0,0,0)")
:border (anim/use-val (if platform/ios? 0 12)) :border (anim/use-val (if platform/ios? 0 12))
:opacity (anim/use-val 0) :opacity (anim/use-val 0)
:rotate (anim/use-val "0deg") :rotate (anim/use-val "0deg")
:top-layout (anim/use-val -10) :layout (anim/use-val -10)
:bottom-layout (anim/use-val 10)
:top-view-y (anim/use-val 0) :top-view-y (anim/use-val 0)
:top-view-x (anim/use-val 0) :top-view-x (anim/use-val 0)
:top-view-width (anim/use-val (:width window)) :top-view-width (anim/use-val window-width)
:top-view-bg (anim/use-val colors/neutral-100-opa-0)} :top-view-bg (anim/use-val colors/neutral-100-opa-0)
:pan-y (anim/use-val 0)
:pan-x (anim/use-val 0)}
derived {:top-layout (worklet/info-layout (:layout animations)
true)
:bottom-layout (worklet/info-layout (:layout animations)
false)}
callback (fn [e] callback (fn [e]
(on-viewable-items-changed e scroll-index atoms)) (on-viewable-items-changed e scroll-index atoms))]
insets-ios (when platform/ios? (safe-area/use-safe-area))]
(anim/animate (:background-color animations) "rgba(0,0,0,1)") (anim/animate (:background-color animations) "rgba(0,0,0,1)")
(reset! data messages) (reset! data messages)
(orientation/use-device-orientation-change (orientation/use-device-orientation-change
(fn [result] (fn [result]
(if platform/ios? (if platform/ios?
(handle-orientation result scroll-index window animations atoms) (handle-orientation result scroll-index window-width window-height animations insets atoms)
;; `use-device-orientation-change` will always be called on Android, so need to check ;; `use-device-orientation-change` will always be called on Android, so need to check
(orientation/get-auto-rotate-state (orientation/get-auto-rotate-state
(fn [enabled?] (fn [enabled?]
;; RNN does not support landscape-right ;; RNN does not support landscape-right
(when (and enabled? (not= result orientation/landscape-right)) (when (and enabled? (not= result orientation/landscape-right))
(handle-orientation result scroll-index window animations atoms))))))) (handle-orientation result
scroll-index
window-width
window-height
animations
insets
atoms)))))))
(rn/use-effect (fn [] (rn/use-effect (fn []
(when @(:flat-list-ref atoms) (when @(:flat-list-ref atoms)
(.scrollToIndex ^js @(:flat-list-ref atoms) (.scrollToIndex ^js @(:flat-list-ref atoms)
#js {:animated false :index index})) #js {:animated false :index index}))
(js/setTimeout (fn [] (js/setTimeout (fn []
(anim/animate (:opacity animations) 1) (anim/animate (:opacity animations) 1)
(anim/animate (:top-layout animations) 0) (anim/animate (:layout animations) 0)
(anim/animate (:bottom-layout animations) 0)
(anim/animate (:border animations) 12)) (anim/animate (:border animations) 12))
(if platform/ios? 250 100)) (if platform/ios? 250 100))
(js/setTimeout #(reset! (:scroll-index-lock? atoms) false) 300) (js/setTimeout #(reset! (:scroll-index-lock? atoms) false) 300)
@ -151,31 +189,34 @@
(rf/dispatch [:chat.ui/zoom-out-signal nil]) (rf/dispatch [:chat.ui/zoom-out-signal nil])
(when platform/android? (when platform/android?
(rf/dispatch [:chat.ui/lightbox-scale 1]))))) (rf/dispatch [:chat.ui/lightbox-scale 1])))))
[container-view [:f>
(fn [insets-android] (fn []
(let [insets (if platform/ios? insets-ios insets-android) (let [curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
landscape? (string/includes? curr-orientation orientation/landscape) landscape? (string/includes? curr-orientation orientation/landscape)
horizontal? (or platform/android? (not landscape?)) horizontal? (or platform/android? (not landscape?))
inverted? (and platform/ios? (= curr-orientation orientation/landscape-right)) inverted? (and platform/ios? (= curr-orientation orientation/landscape-right))
screen-width (if (or platform/ios? (= curr-orientation orientation/portrait)) screen-width (if (or platform/ios? (= curr-orientation orientation/portrait))
(:width window) window-width
(:height window)) window-height)
screen-height (if (or platform/ios? (= curr-orientation orientation/portrait)) screen-height (if (or platform/ios? (= curr-orientation orientation/portrait))
(:height window) window-height
(:width window)) window-width)
item-width (if (and landscape? platform/ios?) screen-height screen-width)] item-width (if (and landscape? platform/ios?) screen-height screen-width)]
(reset! (:insets-atom atoms) insets)
[reanimated/view [reanimated/view
{:style (if platform/ios? {:style (reanimated/apply-animations-to-style {:background-color (:background-color
(reanimated/apply-animations-to-style {:background-color (:background-color
animations)} animations)}
{}) {:height screen-height})}
{:background-color :black})}
(when-not @transparent? (when-not @transparent?
[top-view/top-view (first messages) insets scroll-index animations landscape? [top-view/top-view (first messages) insets scroll-index animations derived landscape?
screen-width]) screen-width])
[rn/flat-list [gesture/gesture-detector
{:gesture (drag-gesture animations (and landscape? platform/ios?) set-full-height?)}
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:transform [{:translateY (:pan-y animations)}
{:translateX (:pan-x animations)}]}
{})}
[gesture/flat-list
{:ref #(reset! (:flat-list-ref atoms) %) {:ref #(reset! (:flat-list-ref atoms) %)
:key-fn :message-id :key-fn :message-id
:style {:width (+ screen-width seperator-width)} :style {:width (+ screen-width seperator-width)}
@ -184,8 +225,11 @@
:render-data {:opacity-value (:opacity animations) :render-data {:opacity-value (:opacity animations)
:border-value (:border animations) :border-value (:border animations)
:transparent? transparent? :transparent? transparent?
:height screen-height :set-full-height? set-full-height?
:width screen-width :screen-height screen-height
:screen-width screen-width
:window-height window-height
:window-width window-width
:atoms atoms} :atoms atoms}
:horizontal horizontal? :horizontal horizontal?
:inverted inverted? :inverted inverted?
@ -195,9 +239,7 @@
:wait-for-interaction true} :wait-for-interaction true}
:shows-vertical-scroll-indicator false :shows-vertical-scroll-indicator false
:shows-horizontal-scroll-indicator false :shows-horizontal-scroll-indicator false
:on-viewable-items-changed callback :on-viewable-items-changed callback}]]]
:content-container-style {:justify-content :center
:align-items :center}}]
(when (and (not @transparent?) (not landscape?)) (when (and (not @transparent?) (not landscape?))
[bottom-view/bottom-view messages index scroll-index insets animations [bottom-view/bottom-view messages index scroll-index insets animations derived
item-width atoms])]))]))]) item-width atoms])]))]))])

View File

@ -37,10 +37,10 @@
{:keys [x-threshold-scale y-threshold-scale]} {:keys [x-threshold-scale y-threshold-scale]}
{:keys [scale saved-scale] :as animations} {:keys [scale saved-scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled?] :as props}] {:keys [pan-x-enabled? pan-y-enabled?] :as props}]
(anim/animate scale value (if exit? 100 c/default-duration))
(anim/set-val saved-scale value)
(when (= value c/min-scale) (when (= value c/min-scale)
(reset-values exit? animations props)) (reset-values exit? animations props))
(anim/animate scale value (if exit? 100 c/default-duration))
(anim/set-val saved-scale value)
(reset! pan-x-enabled? (> value x-threshold-scale)) (reset! pan-x-enabled? (> value x-threshold-scale))
(reset! pan-y-enabled? (> value y-threshold-scale))) (reset! pan-y-enabled? (> value y-threshold-scale)))
@ -51,24 +51,37 @@
{:keys [landscape-scale-val x-threshold-scale y-threshold-scale]} {:keys [landscape-scale-val x-threshold-scale y-threshold-scale]}
{:keys [rotate rotate-scale scale] :as animations} {:keys [rotate rotate-scale scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled?]}] {:keys [pan-x-enabled? pan-y-enabled?]}]
(let [duration (when focused? c/default-duration)] (if focused?
(cond (cond
(= curr-orientation orientation/landscape-left) (= curr-orientation orientation/landscape-left)
(do (do
(anim/animate rotate "90deg" duration) (anim/animate rotate "90deg")
(anim/animate rotate-scale landscape-scale-val duration)) (anim/animate rotate-scale landscape-scale-val))
(= curr-orientation orientation/landscape-right) (= curr-orientation orientation/landscape-right)
(do (do
(anim/animate rotate "-90deg" duration) (anim/animate rotate "-90deg")
(anim/animate rotate-scale landscape-scale-val duration)) (anim/animate rotate-scale landscape-scale-val))
(= curr-orientation orientation/portrait) (= curr-orientation orientation/portrait)
(do (do
(anim/animate rotate c/init-rotation duration) (anim/animate rotate c/init-rotation)
(anim/animate rotate-scale c/min-scale duration))) (anim/animate rotate-scale c/min-scale)))
(cond
(= curr-orientation orientation/landscape-left)
(do
(anim/set-val rotate "90deg")
(anim/set-val rotate-scale landscape-scale-val))
(= curr-orientation orientation/landscape-right)
(do
(anim/set-val rotate "-90deg")
(anim/set-val rotate-scale landscape-scale-val))
(= curr-orientation orientation/portrait)
(do
(anim/set-val rotate c/init-rotation)
(anim/set-val rotate-scale c/min-scale))))
(center-x animations false) (center-x animations false)
(center-y animations false) (center-y animations false)
(reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale)) (reset! pan-x-enabled? (> (anim/get-val scale) x-threshold-scale))
(reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale)))) (reset! pan-y-enabled? (> (anim/get-val scale) y-threshold-scale)))
;; On ios, when attempting to navigate back while zoomed in, the shared-element transition animation ;; On ios, when attempting to navigate back while zoomed in, the shared-element transition animation
;; doesn't execute properly, so we need to zoom out first ;; doesn't execute properly, so we need to zoom out first
@ -94,14 +107,9 @@
"Calculates all required dimensions. Dimensions calculations are different on iOS and Android because landscape "Calculates all required dimensions. Dimensions calculations are different on iOS and Android because landscape
mode is implemented differently.On Android, we just need to resize the content, and the OS takes care of the mode is implemented differently.On Android, we just need to resize the content, and the OS takes care of the
animations. On iOS, we need to animate the content ourselves in code" animations. On iOS, we need to animate the content ourselves in code"
[pixels-width pixels-height curr-orientation] [pixels-width pixels-height curr-orientation
(let [window (rf/sub [:dimensions/window]) {:keys [window-width screen-width screen-height]}]
landscape? (string/includes? curr-orientation orientation/landscape) (let [landscape? (string/includes? curr-orientation orientation/landscape)
portrait? (= curr-orientation orientation/portrait)
window-width (:width window)
window-height (:height window)
screen-width (if (or platform/ios? portrait?) window-width window-height)
screen-height (if (or platform/ios? portrait?) window-height window-width)
portrait-image-width window-width portrait-image-width window-width
portrait-image-height (* pixels-height (/ window-width pixels-width)) portrait-image-height (* pixels-height (/ window-width pixels-width))
landscape-image-width (* pixels-width (/ window-width pixels-height)) landscape-image-width (* pixels-width (/ window-width pixels-height))

View File

@ -22,7 +22,9 @@
(defn double-tap-gesture (defn double-tap-gesture
[{:keys [width height screen-width screen-height y-threshold-scale x-threshold-scale]} [{:keys [width height screen-width screen-height y-threshold-scale x-threshold-scale]}
{:keys [scale pan-x pan-x-start pan-y pan-y-start]} {:keys [scale pan-x pan-x-start pan-y pan-y-start]}
rescale] rescale
transparent?
toggle-opacity]
(-> (->
(gesture/gesture-tap) (gesture/gesture-tap)
(gesture/number-of-taps 2) (gesture/number-of-taps 2)
@ -37,8 +39,13 @@
(when (> c/double-tap-scale y-threshold-scale) (when (> c/double-tap-scale y-threshold-scale)
(anim/animate pan-y translate-y) (anim/animate pan-y translate-y)
(anim/set-val pan-y-start translate-y)) (anim/set-val pan-y-start translate-y))
(rescale c/double-tap-scale)) (rescale c/double-tap-scale)
(rescale c/min-scale)))))) (when (not @transparent?)
(toggle-opacity)))
(do
(rescale c/min-scale)
(when @transparent?
(toggle-opacity))))))))
;; not using on-finalize because on-finalize gets called always regardless the gesture executed or not ;; not using on-finalize because on-finalize gets called always regardless the gesture executed or not
(defn finalize-pinch (defn finalize-pinch
@ -74,7 +81,9 @@
{:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pinch-x-max pinch-y-max] {:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pinch-x-max pinch-y-max]
:as animations} :as animations}
{:keys [focal-x focal-y] :as props} {:keys [focal-x focal-y] :as props}
rescale] rescale
transparent?
toggle-opacity]
(-> (->
(gesture/gesture-pinch) (gesture/gesture-pinch)
(gesture/on-begin (fn [e] (gesture/on-begin (fn [e]
@ -82,6 +91,8 @@
(reset! focal-x (oget e "focalX")) (reset! focal-x (oget e "focalX"))
(reset! focal-y (utils/get-focal (oget e "focalY") height screen-height))))) (reset! focal-y (utils/get-focal (oget e "focalY") height screen-height)))))
(gesture/on-start (fn [e] (gesture/on-start (fn [e]
(when (and (= (anim/get-val saved-scale) c/min-scale) (not @transparent?))
(toggle-opacity))
(when platform/android? (when platform/android?
(reset! focal-x (utils/get-focal (oget e "focalX") width screen-width)) (reset! focal-x (utils/get-focal (oget e "focalX") width screen-width))
(reset! focal-y (utils/get-focal (oget e "focalY") height screen-height))))) (reset! focal-y (utils/get-focal (oget e "focalY") height screen-height)))))
@ -101,7 +112,10 @@
(fn [] (fn []
(cond (cond
(< (anim/get-val scale) c/min-scale) (< (anim/get-val scale) c/min-scale)
(rescale c/min-scale) (do
(when @transparent?
(toggle-opacity))
(rescale c/min-scale))
(> (anim/get-val scale) c/max-scale) (> (anim/get-val scale) c/max-scale)
(do (do
(anim/animate pinch-x (anim/get-val pinch-x-max)) (anim/animate pinch-x (anim/get-val pinch-x-max))
@ -191,16 +205,21 @@
(anim/animate-decay pan-y-start velocity [lower-bound upper-bound])))))))) (anim/animate-decay pan-y-start velocity [lower-bound upper-bound]))))))))
(defn zoomable-image (defn zoomable-image
[{:keys [image-width image-height content message-id]} index border-radius on-tap] [{:keys [image-width image-height content message-id]} index args on-tap]
(let [set-full-height? (reagent/atom false)]
[:f> [:f>
(fn [] (fn []
(let [shared-element-id (rf/sub [:shared-element-id]) (let [{:keys [transparent? set-full-height?]} args
shared-element-id (rf/sub [:shared-element-id])
exit-lightbox-signal (rf/sub [:lightbox/exit-signal]) exit-lightbox-signal (rf/sub [:lightbox/exit-signal])
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal]) zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
focused? (= shared-element-id message-id) focused? (= shared-element-id message-id)
curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait) curr-orientation (or (rf/sub [:lightbox/orientation])
dimensions (utils/get-dimensions image-width image-height curr-orientation) orientation/portrait)
portrait? (= curr-orientation orientation/portrait)
dimensions (utils/get-dimensions image-width
image-height
curr-orientation
args)
animations {:scale (anim/use-val c/min-scale) animations {:scale (anim/use-val c/min-scale)
:saved-scale (anim/use-val c/min-scale) :saved-scale (anim/use-val c/min-scale)
:pan-x-start (anim/use-val c/init-offset) :pan-x-start (anim/use-val c/init-offset)
@ -220,10 +239,13 @@
:focal-x (reagent/atom nil) :focal-x (reagent/atom nil)
:focal-y (reagent/atom nil)} :focal-y (reagent/atom nil)}
rescale (fn [value exit?] rescale (fn [value exit?]
(utils/rescale-image value exit? dimensions animations props))] (utils/rescale-image value
(rn/use-effect-once (fn [] exit?
(js/setTimeout #(reset! set-full-height? true) 500) dimensions
js/undefined)) animations
props))]
(rn/use-effect (fn []
(js/setTimeout #(reset! set-full-height? true) 500)))
(when platform/ios? (when platform/ios?
(utils/handle-orientation-change curr-orientation focused? dimensions animations props) (utils/handle-orientation-change curr-orientation focused? dimensions animations props)
(utils/handle-exit-lightbox-signal exit-lightbox-signal (utils/handle-exit-lightbox-signal exit-lightbox-signal
@ -234,9 +256,11 @@
(utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale) (utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale)
[:f> [:f>
(fn [] (fn []
(let [tap (tap-gesture on-tap) (let [tap (tap-gesture #(on-tap portrait?))
double-tap (double-tap-gesture dimensions animations rescale) double-tap
pinch (pinch-gesture dimensions animations props rescale) (double-tap-gesture dimensions animations rescale transparent? #(on-tap portrait?))
pinch
(pinch-gesture dimensions animations props rescale transparent? #(on-tap portrait?))
pan-x (pan-x-gesture dimensions animations props rescale) pan-x (pan-x-gesture dimensions animations props rescale)
pan-y (pan-y-gesture dimensions animations props rescale) pan-y (pan-y-gesture dimensions animations props rescale)
composed-gestures (gesture/exclusive composed-gestures (gesture/exclusive
@ -251,4 +275,4 @@
[reanimated/fast-image [reanimated/fast-image
{:source {:uri (:image content)} {:source {:uri (:image content)}
:native-ID (when focused? :shared-element) :native-ID (when focused? :shared-element)
:style (style/image dimensions animations border-radius)}]]]))]))])) :style (style/image dimensions animations (:border-value args))}]]]))]))])

View File

@ -3,6 +3,7 @@
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.fast-image :as fast-image] [react-native.fast-image :as fast-image]
[react-native.safe-area :as safe-area]
[status-im2.contexts.chat.messages.content.album.style :as style] [status-im2.contexts.chat.messages.content.album.style :as style]
[status-im2.constants :as constants] [status-im2.constants :as constants]
[status-im2.contexts.chat.messages.content.image.view :as image] [status-im2.contexts.chat.messages.content.image.view :as image]
@ -19,7 +20,10 @@
(defn album-message (defn album-message
[{:keys [albumize?] :as message} context on-long-press] [{:keys [albumize?] :as message} context on-long-press]
(let [shared-element-id (rf/sub [:shared-element-id]) [:f>
(fn []
(let [insets (safe-area/use-safe-area)
shared-element-id (rf/sub [:shared-element-id])
first-image (first (:album message)) first-image (first (:album message))
album-style (if (> (:image-width first-image) (:image-height first-image)) album-style (if (> (:image-width first-image) (:image-height first-image))
:landscape :landscape
@ -37,7 +41,9 @@
{:style (style/album-container portrait?)} {:style (style/album-container portrait?)}
(map-indexed (map-indexed
(fn [index item] (fn [index item]
(let [images-size-key (if (< images-count constants/max-album-photos) images-count :default) (let [images-size-key (if (< images-count constants/max-album-photos)
images-count
:default)
size (get-in constants/album-image-sizes [images-size-key index]) size (get-in constants/album-image-sizes [images-size-key index])
dimensions (if (not= images-count rectangular-style-count) dimensions (if (not= images-count rectangular-style-count)
{:width size :height size} {:width size :height size}
@ -50,7 +56,8 @@
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)]) (rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)])
(js/setTimeout #(rf/dispatch [:navigate-to :lightbox (js/setTimeout #(rf/dispatch [:navigate-to :lightbox
{:messages (:album message) {:messages (:album message)
:index index}]) :index index
:insets insets}])
100))} 100))}
[fast-image/fast-image [fast-image/fast-image
{:style (style/image dimensions index portrait? images-count) {:style (style/image dimensions index portrait? images-count)
@ -72,4 +79,4 @@
(map-indexed (map-indexed
(fn [index item] (fn [index item]
[image/image-message index item context #(on-long-press message context)]) [image/image-message index item context #(on-long-press message context)])
(:album message))]))) (:album message))])))])

View File

@ -2,6 +2,7 @@
(:require (:require
[react-native.core :as rn] [react-native.core :as rn]
[react-native.fast-image :as fast-image] [react-native.fast-image :as fast-image]
[react-native.safe-area :as safe-area]
[status-im2.constants :as constants] [status-im2.constants :as constants]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[status-im2.contexts.chat.messages.content.text.view :as text])) [status-im2.contexts.chat.messages.content.text.view :as text]))
@ -14,23 +15,28 @@
(defn image-message (defn image-message
[index {:keys [content image-width image-height message-id] :as message} context on-long-press] [index {:keys [content image-width image-height message-id] :as message} context on-long-press]
(let [dimensions (calculate-dimensions (or image-width 1000) (or image-height 1000)) [:f>
(fn []
(let [insets (safe-area/use-safe-area)
dimensions (calculate-dimensions (or image-width 1000) (or image-height 1000))
text (:text content)] text (:text content)]
(fn [] (fn []
(let [shared-element-id (rf/sub [:shared-element-id])] (let [shared-element-id (rf/sub [:shared-element-id])]
[rn/touchable-opacity [rn/touchable-opacity
{:active-opacity 1 {:active-opacity 1
:key message-id :key message-id
:style {:margin-top (when (> index 0) 10)} :style {:margin-top (when (pos? index) 10)}
:on-long-press on-long-press :on-long-press on-long-press
:on-press (fn [] :on-press (fn []
(rf/dispatch [:chat.ui/update-shared-element-id message-id]) (rf/dispatch [:chat.ui/update-shared-element-id message-id])
(js/setTimeout #(rf/dispatch [:navigate-to :lightbox (js/setTimeout #(rf/dispatch [:navigate-to :lightbox
{:messages [message] :index 0}]) {:messages (:album message)
:index index
:insets insets}])
100))} 100))}
(when (and (not= text "placeholder") (= index 0)) (when (and (not= text "placeholder") (= index 0))
[rn/view {:style {:margin-bottom 10}} [text/text-content message context]]) [rn/view {:style {:margin-bottom 10}} [text/text-content message context]])
[fast-image/fast-image [fast-image/fast-image
{:source {:uri (:image content)} {:source {:uri (:image content)}
:style (merge dimensions {:border-radius 12}) :style (merge dimensions {:border-radius 12})
:native-ID (when (= shared-element-id message-id) :shared-element)}]])))) :native-ID (when (= shared-element-id message-id) :shared-element)}]]))))])

View File

@ -61,9 +61,11 @@
{:name :lightbox {:name :lightbox
:insets {:top false :bottom false} :insets {:top false :bottom false}
:options {:topBar {:visible false} :options {:topBar {:visible false}
:statusBar {:backgroundColor colors/black :statusBar {:backgroundColor :transparent
:style :light :style :light
:animate true} :animate true
:drawBehind true
:translucent true}
:navigationBar {:backgroundColor colors/black} :navigationBar {:backgroundColor colors/black}
:layout {:componentBackgroundColor :transparent :layout {:componentBackgroundColor :transparent
:backgroundColor :transparent} :backgroundColor :transparent}

View File

@ -0,0 +1,7 @@
(ns utils.worklets.lightbox)
(def ^:private layout-worklets (js/require "../src/js/worklets/lightbox.js"))
(defn info-layout
[input top?]
(.infoLayout ^js layout-worklets input top?))