Landscape Mode (#15175)

* feat: landscape mode
This commit is contained in:
Omar Basem 2023-03-03 16:33:28 +04:00 committed by GitHub
parent 52b87bab3e
commit 8923408972
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 696 additions and 329 deletions

View File

@ -61,6 +61,7 @@
"react-native-lottie-splash-screen": "^1.0.1",
"react-native-mail": "^6.1.1",
"react-native-navigation": "^7.27.1",
"react-native-orientation-locker": "^1.5.0",
"react-native-permissions": "^2.1.5",
"react-native-randombytes": "^3.6.1",
"react-native-reanimated": "2.3.3",

View File

@ -334,6 +334,10 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
(def react-native-camera-roll
(clj->js {:default #js {}}))
(def react-native-orientation-locker
(clj->js {:default #js {}
:useDeviceOrientationChange #js {}}))
(def wallet-connect-client
#js
{:default #js {}
@ -399,6 +403,7 @@ globalThis.__STATUS_MOBILE_JS_IDENTITY_PROXY__ = new Proxy({}, {get() { return (
"react-native-share" react-native-share
"@react-native-async-storage/async-storage" async-storage
"react-native-svg" react-native-svg
"react-native-orientation-locker" react-native-orientation-locker
"../src/js/worklet_factory.js" worklet-factory
"../src/js/shell_worklets.js" shell-worklets
"../src/js/bottom_sheet.js" bottom-sheet

View File

@ -26,6 +26,8 @@
(defn on-end [gesture handler] (.onEnd ^js gesture handler))
(defn on-finalize [gesture handler] (.onFinalize ^js gesture handler))
(defn number-of-taps [gesture count] (.numberOfTaps ^js gesture count))
(defn enabled [gesture enabled?] (.enabled ^js gesture enabled?))

View File

@ -0,0 +1,32 @@
(ns react-native.orientation
(:require ["react-native-orientation-locker" :default orientation :refer (useDeviceOrientationChange)]
[react-native.navigation :as navigation]))
(def use-device-orientation-change useDeviceOrientationChange)
(def get-auto-rotate-state (.-getAutoRotateState orientation))
(def portrait-options
(clj->js {:layout {:orientation ["portrait"]}
:statusBar {:visible true}}))
(def landscape-option-1 (clj->js {:layout {:orientation ["landscape"]}}))
(def landscape-option-2 (clj->js {:statusBar {:visible false}}))
(defn lock-to-portrait
[id]
(navigation/merge-options id portrait-options))
(defn lock-to-landscape
[id]
(navigation/merge-options id landscape-option-1)
;; On Android, hiding the status-bar while changing orientation causes a flicker, so we enqueue it
(js/setTimeout #(navigation/merge-options id landscape-option-2) 0))
(def ^:const portrait "PORTRAIT")
(def ^:const landscape "LANDSCAPE")
(def ^:const landscape-left "LANDSCAPE-LEFT")
(def ^:const landscape-right "LANDSCAPE-RIGHT")

View File

@ -57,9 +57,12 @@
;; Easings
(def bezier (.-bezier ^js Easing))
(def in-out
(.-inOut ^js Easing))
(def easings
{:linear (bezier 0 0 1 1)
:easing1 (bezier 0.25 0.1 0.25 1) ;; TODO(parvesh) - rename easing functions, (design team input)
:easing1 (bezier 0.25 0.1 0.25 1)
:easing2 (bezier 0 0.3 0.6 0.9)
:easing3 (bezier 0.3 0.3 0.3 0.9)})
@ -109,6 +112,14 @@
(js-obj "duration" duration
"easing" (get easings easing))))))
(defn animate-shared-value-with-delay-default-easing
[anim val duration delay]
(set-shared-value anim
(with-delay delay
(with-timing val
(js-obj "duration" duration
"easing" (in-out (.-quad ^js Easing)))))))
(defn animate-shared-value-with-repeat
[anim val duration easing number-of-repetitions reverse?]
(set-shared-value anim
@ -146,9 +157,6 @@
(with-decay (clj->js {:velocity velocity
:clamp clamp}))))
(def in-out
(.-inOut Easing))
(defn with-timing-duration
[val duration]
(with-timing val

View File

@ -347,3 +347,9 @@
{:events [:chat.ui/zoom-out-signal]}
[{:keys [db]} value]
{:db (assoc db :lightbox/zoom-out-signal value)})
(rf/defn orientation-change
{:events [:chat.ui/orientation-change]}
[{:keys [db]} value]
{:db (assoc db :lightbox/orientation value)})

View File

@ -0,0 +1,78 @@
(ns status-im2.contexts.chat.lightbox.bottom-view
(:require
[react-native.core :as rn]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.style :as style]
[utils.re-frame :as rf]
[status-im2.contexts.chat.lightbox.common :as common]))
(def small-image-size 40)
(def focused-image-size 56)
(def small-list-height 80)
(defn get-small-item-layout
[_ index]
#js
{:length small-image-size
:offset (* (+ small-image-size 8) index)
:index index})
(defn small-image
[item index _ {:keys [scroll-index atoms]}]
[:f>
(fn []
(let [size (if (= @scroll-index index) focused-image-size small-image-size)
size-value (common/use-val size)
{:keys [scroll-index-lock? small-list-ref
flat-list-ref]} atoms]
(common/set-val-timing size-value size)
[rn/touchable-opacity
{:active-opacity 1
:on-press (fn []
(rf/dispatch [:chat.ui/zoom-out-signal @scroll-index])
(reset! scroll-index-lock? true)
(js/setTimeout #(reset! scroll-index-lock? false) 500)
(js/setTimeout
(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}))
(if platform/ios? 50 150))
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)]))}
[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
[messages index scroll-index insets animations item-width atoms]
[:f>
(fn []
(let [text (get-in (first messages) [:content :text])
padding-horizontal (- (/ item-width 2) (/ focused-image-size 2))]
[reanimated/linear-gradient
{:colors [:black :transparent]
:start {:x 0 :y 1}
:end {:x 0 :y 0}
:style (style/gradient-container insets animations)}
[rn/text
{:style style/text-style} text]
[rn/flat-list
{:ref #(reset! (:small-list-ref atoms) %)
:key-fn :message-id
:style {:height small-list-height}
:data messages
:render-fn small-image
:render-data {:scroll-index scroll-index
:atoms atoms}
:horizontal true
:get-item-layout get-small-item-layout
:separator [rn/view {:style {:width 8}}]
:initial-scroll-index index
:content-container-style (style/content-container padding-horizontal)}]]))])

View File

@ -0,0 +1,14 @@
(ns status-im2.contexts.chat.lightbox.common
(:require [react-native.reanimated :as reanimated]))
(def top-view-height 56)
;; TODO: Abstract Reanimated methods in a better way, issue:
;; https://github.com/status-im/status-mobile/issues/15176
(defn set-val-timing
[animation value]
(reanimated/set-shared-value animation (reanimated/with-timing value)))
(defn use-val
[value]
(reanimated/use-shared-value value))

View File

@ -1,22 +1,39 @@
(ns status-im2.contexts.chat.lightbox.style
(:require [quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]))
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.common :as common]))
;;;; MAIN-VIEW
(def container-view
{:background-color :black
:height "100%"})
{:background-color :black})
;;;; TOP-VIEW
(defn top-view-container
[top-inset opacity]
[top-inset {:keys [opacity rotate top-view-y top-view-x top-view-width top-view-bg]} window-width
bg-color]
(reanimated/apply-animations-to-style
{:opacity opacity}
{:position :absolute
:left 20
:top (+ 12 top-inset)
:z-index 4
:flex-direction :row
:width "100%"}))
(if platform/ios?
{:transform [{:rotate rotate}
{:translateY top-view-y}
{:translateX top-view-x}]
:opacity opacity
:width top-view-width
:background-color top-view-bg}
{:transform [{:rotate rotate}
{:translateY top-view-y}
{:translateX top-view-x}]
:opacity opacity})
{:position :absolute
:padding-horizontal 20
:top (if platform/ios? top-inset 0)
:height common/top-view-height
:z-index 4
:flex-direction :row
:justify-content :space-between
:width (when platform/android? window-width)
:background-color (when platform/android? bg-color)
:align-items :center}))
(def close-container
{:width 32
@ -27,20 +44,25 @@
:background-color colors/neutral-80-opa-40})
(def top-right-buttons
{:position :absolute
:right 40
:flex-direction :row})
{:flex-direction :row})
;;;; BOTTOM-VIEW
(defn gradient-container
[insets opacity]
[insets {:keys [opacity]}]
(reanimated/apply-animations-to-style
{:opacity opacity}
{:width "100%"
:position :absolute
{:position :absolute
:bottom 0
:padding-bottom (:bottom insets)
:z-index 3}))
(defn content-container
[padding-horizontal]
{:padding-vertical 12
:padding-horizontal padding-horizontal
:align-items :center
:justify-content :center})
(def text-style
{:color colors/white
:align-self :center

View File

@ -0,0 +1,75 @@
(ns status-im2.contexts.chat.lightbox.top-view
(:require
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.orientation :as orientation]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.common :as common]
[status-im2.contexts.chat.lightbox.style :as style]
[utils.datetime :as datetime]
[utils.re-frame :as rf]))
(defn animate-rotation
[result screen-width screen-height insets-atom
{:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}]
(let [top-x (+ (/ common/top-view-height 2) (:top insets-atom))]
(cond
(= result orientation/landscape-left)
(do
(common/set-val-timing rotate "90deg")
(common/set-val-timing top-view-y 60)
(common/set-val-timing top-view-x (- (/ screen-height 2) top-x))
(common/set-val-timing top-view-width screen-height)
(common/set-val-timing top-view-bg colors/neutral-100-opa-70))
(= result orientation/landscape-right)
(do
(common/set-val-timing rotate "-90deg")
(common/set-val-timing top-view-y (- (- screen-width) 4))
(common/set-val-timing top-view-x (+ (/ screen-height -2) top-x))
(common/set-val-timing top-view-width screen-height)
(common/set-val-timing top-view-bg colors/neutral-100-opa-70))
(= result orientation/portrait)
(do
(common/set-val-timing rotate "0deg")
(common/set-val-timing top-view-y 0)
(common/set-val-timing top-view-x 0)
(common/set-val-timing top-view-width screen-width)
(common/set-val-timing top-view-bg colors/neutral-100-opa-0)))))
(defn top-view
[{:keys [from timestamp]} insets index animations landscape? screen-width]
[:f>
(fn []
(let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from]))
bg-color (if landscape? colors/neutral-100-opa-70 colors/neutral-100-opa-0)]
[reanimated/view
{:style (style/top-view-container (:top insets) animations screen-width bg-color)}
[rn/view
{:style {:flex-direction :row
:align-items :center}}
[rn/touchable-opacity
{:on-press #(rf/dispatch (if platform/ios?
[:chat.ui/exit-lightbox-signal @index]
[:navigate-back]))
:style style/close-container}
[quo/icon :close {:size 20 :color colors/white}]]
[rn/view {:style {:margin-left 12}}
[quo/text
{:weight :semi-bold
:size :paragraph-1
:style {:color colors/white}} display-name]
[quo/text
{:weight :medium
:size :paragraph-2
:style {:color colors/neutral-40}} (datetime/to-short-str timestamp)]]]
[rn/view {:style style/top-right-buttons}
[rn/touchable-opacity
{:active-opacity 1
:style (merge style/close-container {:margin-right 12})}
[quo/icon :share {:size 20 :color colors/white}]]
[rn/touchable-opacity
{:active-opacity 1
:style style/close-container}
[quo/icon :options {:size 20 :color colors/white}]]]]))])

View File

@ -1,188 +1,175 @@
(ns status-im2.contexts.chat.lightbox.view
(:require
[quo2.core :as quo]
[clojure.string :as string]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.orientation :as orientation]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.lightbox.common :as common]
[utils.re-frame :as rf]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im2.contexts.chat.lightbox.style :as style]
[utils.datetime :as datetime]
[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.bottom-view :as bottom-view]
[oops.core :refer [oget]]))
(def flat-list-ref (atom nil))
(def small-list-ref (atom nil))
(def small-image-size 40)
(def focused-image-size 56)
(def seperator-width 16)
(defn toggle-opacity
[opacity-value border-value transparent? index]
[opacity-value border-value transparent? index {:keys [small-list-ref]}]
(let [opacity (reanimated/get-shared-value opacity-value)]
(if (= opacity 1)
(do
(reanimated/set-shared-value opacity-value (reanimated/with-timing 0))
(common/set-val-timing opacity-value 0)
(js/setTimeout #(reset! transparent? (not @transparent?)) 400))
(do
(reset! transparent? (not @transparent?))
(js/setTimeout #(reanimated/set-shared-value opacity-value (reanimated/with-timing 1)) 50)
(js/setTimeout #(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}) 100)))
(reanimated/set-shared-value border-value (reanimated/with-timing (if (= opacity 1) 0 12)))))
(defn image
[message index _ {:keys [opacity-value border-value transparent?]}]
[rn/view {:style {:flex-direction :row}}
[zoomable-image/zoomable-image message index border-value
#(toggle-opacity opacity-value border-value transparent? index)]
[rn/view {:style {:width 16}}]])
(reanimated/animate-shared-value-with-delay-default-easing opacity-value 1 300 50)
(js/setTimeout #(when @small-list-ref
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
100)))
(common/set-val-timing border-value (if (= opacity 1) 0 12))))
(defn handle-orientation
[result index window animations {:keys [flat-list-ref insets-atom]}]
(let [screen-width (if (or platform/ios? (= result orientation/portrait))
(:width window)
(:height window))
screen-height (if (or platform/ios? (= result orientation/portrait))
(:height window)
(:width window))
landscape? (string/includes? result orientation/landscape)
item-width (if (and landscape? platform/ios?) screen-height screen-width)
timeout (if platform/ios? 50 100)]
(when (or landscape? (= result orientation/portrait))
(rf/dispatch [:chat.ui/orientation-change result]))
(cond
landscape?
(orientation/lock-to-landscape "lightbox")
(= result orientation/portrait)
(orientation/lock-to-portrait "lightbox"))
(js/setTimeout #(when @flat-list-ref
(.scrollToOffset
^js @flat-list-ref
#js {:animated false :offset (* (+ item-width seperator-width) @index)}))
timeout)
(when platform/ios?
(top-view/animate-rotation result screen-width screen-height @insets-atom animations))))
(defn get-item-layout
[_ index]
(let [window-width (:width (rn/get-window))]
#js {:length window-width :offset (* (+ window-width 16) index) :index index}))
(defn get-small-item-layout
[_ index]
#js {:length small-image-size :offset (* (+ small-image-size 8) index) :index index})
[_ index item-width]
#js {:length item-width :offset (* (+ item-width seperator-width) index) :index index})
(defn on-viewable-items-changed
[e scroll-index]
(let [changed (-> e (oget :changed) first)
index (oget changed :index)]
(reset! scroll-index index)
(when @small-list-ref (.scrollToIndex ^js @small-list-ref #js {:animated true :index index}))
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oget changed :item))])))
[e scroll-index {:keys [scroll-index-lock? small-list-ref]}]
(when-not @scroll-index-lock?
(let [changed (-> e (oget :changed) first)
index (oget changed :index)]
(reset! scroll-index index)
(when @small-list-ref
(.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
[{:keys [from timestamp]} insets opacity-value index]
(defn image
[message index _ {:keys [opacity-value border-value transparent? width height atoms]}]
[:f>
(fn []
(let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from]))]
[reanimated/view
{:style (style/top-view-container (:top insets) opacity-value)}
[rn/touchable-opacity
{:on-press #(rf/dispatch (if platform/ios?
[:chat.ui/exit-lightbox-signal @index]
[:navigate-back]))
:style style/close-container}
[quo/icon :close {:size 20 :color colors/white}]]
[rn/view {:style {:margin-left 12}}
[quo/text
{:weight :semi-bold
:size :paragraph-1
:style {:color colors/white}} display-name]
[quo/text
{:weight :medium
:size :paragraph-2
:style {:color colors/neutral-40}} (datetime/to-short-str timestamp)]]
[rn/view {:style style/top-right-buttons}
[rn/touchable-opacity
{:active-opacity 1
:on-press #(js/alert "to be implemented")
:style (merge style/close-container {:margin-right 12})}
[quo/icon :share {:size 20 :color colors/white}]]
[rn/touchable-opacity
{:active-opacity 1
:on-press #(js/alert "to be implemented")
:style style/close-container}
[quo/icon :options {:size 20 :color colors/white}]]]]))])
(defn small-image
[item index scroll-index]
[:f>
(fn []
(let [size (if (= @scroll-index index) focused-image-size small-image-size)
size-value (reanimated/use-shared-value size)]
(reanimated/set-shared-value size-value (reanimated/with-timing size))
[rn/touchable-opacity
{:active-opacity 1
:on-press (fn []
(rf/dispatch [:chat.ui/zoom-out-signal @scroll-index])
(js/setTimeout
(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}))
(if platform/ios? 50 150)))}
[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
[messages index scroll-index insets opacity-value]
[:f>
(fn []
(let [text (get-in (first messages) [:content :text])
padding-horizontal (- (/ (:width (rn/get-window)) 2) (/ focused-image-size 2))]
[reanimated/linear-gradient
{:colors [:black :transparent]
:start {:x 0 :y 1}
:end {:x 0 :y 0}
:style (style/gradient-container insets opacity-value)}
[rn/text
{:style style/text-style} text]
[rn/flat-list
{:ref #(reset! small-list-ref %)
:key-fn :message-id
:style {:height 68}
:data messages
:render-fn (fn [item index] [small-image item index scroll-index])
:horizontal true
: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}}]]))])
[rn/view
{:style {:flex-direction :row
:width (+ width seperator-width)
:height height
:align-items :center
: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}}]])])
(defn lightbox
[]
[:f>
(fn []
(let [{:keys [messages index]} (rf/sub [:get-screen-params])
atoms {:flat-list-ref (atom nil)
:small-list-ref (atom nil)
:scroll-index-lock? (atom false)
:insets-atom (atom nil)}
;; 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
;; outside the screen (even if we have `initialScrollIndex` set).
data (reagent/atom [(nth messages index)])
scroll-index (reagent/atom index)
transparent? (reagent/atom false)
opacity-value (reanimated/use-shared-value 1)
border-value (reanimated/use-shared-value 12)
window-width (:width (rn/get-window))
window (rf/sub [:dimensions/window])
animations {:border (common/use-val 12)
:opacity (common/use-val 1)
:rotate (common/use-val "0deg")
:top-view-y (common/use-val 0)
:top-view-x (common/use-val 0)
:top-view-width (common/use-val (:width window))
:top-view-bg (common/use-val colors/neutral-100-opa-0)}
callback (fn [e]
(on-viewable-items-changed e
scroll-index))]
(on-viewable-items-changed e scroll-index atoms))]
(reset! data messages)
(orientation/use-device-orientation-change
(fn [result]
(if platform/ios?
(handle-orientation result scroll-index window animations atoms)
;; `use-device-orientation-change` will always be called on Android, so need to check
(orientation/get-auto-rotate-state
(fn [enabled?]
;; RNN does not support landscape-right
(when (and enabled? (not= result orientation/landscape-right))
(handle-orientation result scroll-index window animations atoms)))))))
(rn/use-effect-once (fn []
(.scrollToIndex ^js @flat-list-ref #js {:animated false :index index})
(when @(:flat-list-ref atoms)
(.scrollToIndex ^js @(:flat-list-ref atoms)
#js {:animated false :index index}))
js/undefined))
[safe-area/consumer
(fn [insets]
[rn/view {:style style/container-view}
(when-not @transparent?
[top-view (first messages) insets opacity-value scroll-index])
[rn/flat-list
{:ref #(reset! flat-list-ref %)
:key-fn :message-id
:style {:width (+ window-width 16)}
:data @data
:render-fn image
:render-data {:opacity-value opacity-value
:border-value border-value
:transparent? transparent?}
:horizontal true
:paging-enabled true
:get-item-layout get-item-layout
:viewability-config {:view-area-coverage-percent-threshold 50}
:on-viewable-items-changed callback
:content-container-style {:justify-content :center
:align-items :center}}]
(when-not @transparent?
[bottom-view messages index scroll-index insets opacity-value])])]))])
(let [curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
landscape? (string/includes? curr-orientation orientation/landscape)
horizontal? (or platform/android? (not landscape?))
inverted? (and platform/ios? (= curr-orientation orientation/landscape-right))
screen-width (if (or platform/ios? (= curr-orientation orientation/portrait))
(:width window)
(:height window))
screen-height (if (or platform/ios? (= curr-orientation orientation/portrait))
(:height window)
(:width window))
item-width (if (and landscape? platform/ios?) screen-height screen-width)]
(reset! (:insets-atom atoms) insets)
[rn/view {:style style/container-view}
(when-not @transparent?
[top-view/top-view (first messages) insets scroll-index animations landscape?
screen-width])
[rn/flat-list
{:ref #(reset! (:flat-list-ref atoms) %)
:key-fn :message-id
:style {:width (+ screen-width seperator-width)}
:data @data
:render-fn image
:render-data {:opacity-value (:opacity animations)
:border-value (:border animations)
:transparent? transparent?
:height screen-height
:width screen-width
:atoms atoms}
:horizontal horizontal?
:inverted inverted?
:paging-enabled true
:get-item-layout (fn [_ index] (get-item-layout _ index item-width))
:viewability-config {:view-area-coverage-percent-threshold 50
:wait-for-interaction true}
:shows-vertical-scroll-indicator false
:shows-horizontal-scroll-indicator false
:on-viewable-items-changed callback
:content-container-style {:justify-content :center
:align-items :center}}]
(when (and (not @transparent?) (not landscape?))
[bottom-view/bottom-view messages index scroll-index insets animations
item-width atoms])]))]))])

View File

@ -0,0 +1,15 @@
(ns status-im2.contexts.chat.lightbox.zoomable-image.constants)
(def ^:const min-scale 1)
(def ^:const double-tap-scale 2)
(def ^:const max-scale 5)
(def ^:const init-offset 0)
(def ^:const init-rotation "0deg")
(def ^:const velocity-factor 0.5)
(def ^:const default-duration 300)

View File

@ -0,0 +1,27 @@
(ns status-im2.contexts.chat.lightbox.zoomable-image.style
(:require [react-native.reanimated :as reanimated]))
(defn container
[{:keys [width height]}
{:keys [pan-x pan-y pinch-x pinch-y scale]}]
(reanimated/apply-animations-to-style
{:transform [{:translateX pan-x}
{:translateY pan-y}
{:translateX pinch-x}
{:translateY pinch-y}
{:scale scale}]}
{:justify-content :center
:align-items :center
:width width
:height height}))
(defn image
[{:keys [image-width image-height]}
{:keys [rotate rotate-scale]}
border-radius]
(reanimated/apply-animations-to-style
{:transform [{:rotate rotate}
{:scale rotate-scale}]
:border-radius border-radius}
{:width image-width
:height image-height}))

View File

@ -0,0 +1,83 @@
(ns status-im2.contexts.chat.lightbox.zoomable-image.utils
(:require
[clojure.string :as string]
[react-native.orientation :as orientation]
[react-native.platform :as platform]
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as c]
[utils.re-frame :as rf]))
(defn get-dimensions
"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
animations. On iOS, we need to animate the content ourselves in code"
[pixels-width pixels-height curr-orientation]
(let [window (rf/sub [:dimensions/window])
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-height (* pixels-height (/ window-width pixels-width))
landscape-image-width (* pixels-width (/ window-width pixels-height))
width (if landscape? landscape-image-width portrait-image-width)
height (if landscape? screen-height portrait-image-height)
container-width (if platform/ios? window-width width)
container-height (if (and platform/ios? landscape?) landscape-image-width height)]
;; width and height used in style prop
{:image-width (if platform/ios? portrait-image-width width)
:image-height (if platform/ios? portrait-image-height height)
;; container width and height, also used in animations calculations
:width container-width
:height container-height
;; screen width and height used in calculations, and depends on platform
:screen-width screen-width
:screen-height screen-height
:x-threshold-scale (/ screen-width (min screen-width container-width))
:y-threshold-scale (/ screen-height (min screen-height container-height))
:landscape-scale-val (/ portrait-image-width portrait-image-height)}))
(defn handle-exit-lightbox-signal
"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"
[exit-lightbox-signal index scale rescale]
(when (= exit-lightbox-signal index)
(if (> scale c/min-scale)
(do
(rescale c/min-scale true)
(js/setTimeout #(rf/dispatch [:navigate-back]) 70))
(rf/dispatch [:navigate-back]))
(js/setTimeout #(rf/dispatch [:chat.ui/exit-lightbox-signal nil]) 500)))
(defn handle-zoom-out-signal
"Zooms out when pressing on another photo from the small bottom list"
[zoom-out-signal index scale rescale]
(when (and (= zoom-out-signal index) (> scale c/min-scale))
(rescale c/min-scale true)))
;;; MATH
(defn get-max-offset
[size screen-size scale]
(/ (- (* size (min scale c/max-scale))
screen-size)
2))
(defn get-scale-diff
[new-scale saved-scale]
(- (dec new-scale)
(dec saved-scale)))
(defn get-double-tap-offset
[size screen-size focal]
(let [center (/ size 2)
target-point (* (- center focal) c/double-tap-scale)
max-offset (get-max-offset size screen-size c/double-tap-scale)
translate-val (min (Math/abs target-point) max-offset)]
(if (neg? target-point) (- translate-val) translate-val)))
(defn get-pinch-position
[scale-diff size focal]
(* (- (/ size 2) focal) scale-diff))

View File

@ -1,27 +1,19 @@
(ns status-im2.contexts.chat.lightbox.zoomable-image.view
(:require
[react-native.core :as rn]
[react-native.gesture :as gesture]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[utils.re-frame :as rf]
[oops.core :refer [oget]]))
;;;; Definitions
(def min-scale 1)
(def double-tap-scale 2)
(def max-scale 5)
(def init-offset 0)
(def velocity-factor 0.5)
(def default-duration 300)
[oops.core :refer [oget]]
[react-native.orientation :as orientation]
[status-im2.contexts.chat.lightbox.zoomable-image.constants :as c]
[status-im2.contexts.chat.lightbox.zoomable-image.style :as style]
[status-im2.contexts.chat.lightbox.zoomable-image.utils :as utils]))
;;;; Some aliases for reanimated methods, as they are used 10s of times in this file
;; TODO: Abstract Reanimated methods in a better way, issue:
;; https://github.com/status-im/status-mobile/issues/15176
(defn get-val
[animation]
(reanimated/get-shared-value animation))
@ -36,40 +28,79 @@
(defn timing
([value]
(timing value default-duration))
(timing value c/default-duration))
([value duration]
(reanimated/with-timing-duration value duration)))
(if (= duration nil)
value
(reanimated/with-timing-duration value duration))))
(defn set-val-decay
[animation velocity bounds]
(reanimated/animate-shared-value-with-decay animation (* velocity velocity-factor) bounds))
(reanimated/animate-shared-value-with-decay animation (* velocity c/velocity-factor) bounds))
;;;; MATH
(defn get-max-offset
[size screen-size scale]
(/ (- (* size (min scale max-scale))
screen-size)
2))
;;;; Helpers
(defn center-x
[{:keys [pinch-x pinch-x-start pan-x pan-x-start]} exit?]
(let [duration (if exit? 100 c/default-duration)]
(set-val pinch-x (timing c/init-offset duration))
(set-val pinch-x-start c/init-offset)
(set-val pan-x (timing c/init-offset duration))
(set-val pan-x-start c/init-offset)))
(defn get-scale-diff
[new-scale saved-scale]
(- (dec new-scale)
(dec saved-scale)))
(defn center-y
[{:keys [pinch-y pinch-y-start pan-y pan-y-start]} exit?]
(let [duration (if exit? 100 c/default-duration)]
(set-val pinch-y (timing c/init-offset duration))
(set-val pinch-y-start c/init-offset)
(set-val pan-y (timing c/init-offset duration))
(set-val pan-y-start c/init-offset)))
(defn get-double-tap-offset
[size screen-size focal]
(let [center (/ size 2)
target-point (* (- center focal) double-tap-scale)
max-offset (get-max-offset size screen-size double-tap-scale)
translate-val (min (Math/abs target-point) max-offset)]
(if (neg? target-point) (- translate-val) translate-val)))
(defn reset-values
[exit? animations {:keys [focal-x focal-y]}]
(center-x animations exit?)
(center-y animations exit?)
(reset! focal-x nil)
(reset! focal-y nil))
(defn get-pinch-position
[scale-diff size focal]
(* (- (/ size 2) focal) scale-diff))
(defn rescale-image
[value
exit?
{:keys [x-threshold-scale y-threshold-scale]}
{:keys [scale saved-scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled?] :as props}]
(set-val scale (timing value (if exit? 100 c/default-duration)))
(set-val saved-scale value)
(when (= value c/min-scale)
(reset-values exit? animations props))
(reset! pan-x-enabled? (> value x-threshold-scale))
(reset! pan-y-enabled? (> value y-threshold-scale)))
(defn handle-orientation-change
[curr-orientation
focused?
{:keys [landscape-scale-val x-threshold-scale y-threshold-scale]}
{:keys [rotate rotate-scale scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled?]}]
(let [duration (when focused? c/default-duration)]
(cond
(= curr-orientation orientation/landscape-left)
(do
(set-val rotate (timing "90deg" duration))
(set-val rotate-scale (timing landscape-scale-val duration)))
(= curr-orientation orientation/landscape-right)
(do
(set-val rotate (timing "-90deg" duration))
(set-val rotate-scale (timing landscape-scale-val duration)))
(= curr-orientation orientation/portrait)
(do
(set-val rotate (timing c/init-rotation duration))
(set-val rotate-scale (timing c/min-scale duration))))
(center-x animations false)
(center-y animations false)
(reset! pan-x-enabled? (> (get-val scale) x-threshold-scale))
(reset! pan-y-enabled? (> (get-val scale) y-threshold-scale))))
;;;; 5 Gestures: tap, double-tap, pinch, pan-x, pan-y
;;;; Gestures
(defn tap-gesture
[on-tap]
(->
@ -77,30 +108,32 @@
(gesture/on-start #(on-tap))))
(defn double-tap-gesture
[{:keys [width height screen-height]}
[{: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 [y-threshold-scale x-threshold-scale]}
rescale]
(->
(gesture/gesture-tap)
(gesture/number-of-taps 2)
(gesture/on-start (fn [e]
(if (= (get-val scale) min-scale)
(let [translate-x (get-double-tap-offset width width (oget e "x"))
translate-y (get-double-tap-offset height screen-height (oget e "y"))]
(when (> double-tap-scale x-threshold-scale)
(set-val pan-x (timing translate-x))
(set-val pan-x-start translate-x))
(when (> double-tap-scale y-threshold-scale)
(set-val pan-y (timing translate-y))
(set-val pan-y-start translate-y))
(rescale double-tap-scale))
(rescale min-scale))))))
(gesture/on-start
(fn [e]
(if (= (get-val scale) c/min-scale)
(let [translate-x (utils/get-double-tap-offset width screen-width (oget e "x"))
translate-y (utils/get-double-tap-offset height screen-height (oget e "y"))]
(when (> c/double-tap-scale x-threshold-scale)
(set-val pan-x (timing translate-x))
(set-val pan-x-start translate-x))
(when (> c/double-tap-scale y-threshold-scale)
(set-val pan-y (timing translate-y))
(set-val pan-y-start translate-y))
(rescale c/double-tap-scale))
(rescale c/min-scale))))))
(defn pinch-gesture
[{:keys [width height]}
{:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pinch-x-max pinch-y-max]}
{:keys [pan-x-enabled? pan-y-enabled? x-threshold-scale y-threshold-scale focal-x focal-y]}
[{:keys [width height screen-height screen-width x-threshold-scale y-threshold-scale]}
{:keys [saved-scale scale pinch-x pinch-y pinch-x-start pinch-y-start pinch-x-max pinch-y-max pan-y
pan-y-start pan-x pan-x-start]
:as animations}
{:keys [pan-x-enabled? pan-y-enabled? focal-x focal-y]}
rescale]
(->
(gesture/gesture-pinch)
@ -114,10 +147,10 @@
(reset! focal-y (oget e "focalY")))))
(gesture/on-update (fn [e]
(let [new-scale (* (oget e "scale") (get-val saved-scale))
scale-diff (get-scale-diff new-scale (get-val saved-scale))
new-pinch-x (get-pinch-position scale-diff width @focal-x)
new-pinch-y (get-pinch-position scale-diff height @focal-y)]
(when (and (>= new-scale max-scale) (= (get-val pinch-x-max) js/Infinity))
scale-diff (utils/get-scale-diff new-scale (get-val saved-scale))
new-pinch-x (utils/get-pinch-position scale-diff width @focal-x)
new-pinch-y (utils/get-pinch-position scale-diff height @focal-y)]
(when (and (>= new-scale c/max-scale) (= (get-val pinch-x-max) js/Infinity))
(set-val pinch-x-max (get-val pinch-x))
(set-val pinch-y-max (get-val pinch-y)))
(set-val pinch-x (+ new-pinch-x (get-val pinch-x-start)))
@ -126,9 +159,9 @@
(gesture/on-end
(fn []
(cond
(< (get-val scale) min-scale)
(rescale min-scale)
(> (get-val scale) max-scale)
(< (get-val scale) c/min-scale)
(rescale c/min-scale)
(> (get-val scale) c/max-scale)
(do
(set-val pinch-x (timing (get-val pinch-x-max)))
(set-val pinch-x-start (get-val pinch-x-max))
@ -136,22 +169,44 @@
(set-val pinch-y (timing (get-val pinch-y-max)))
(set-val pinch-y-start (get-val pinch-y-max))
(set-val pinch-y-max js/Infinity)
(set-val scale (timing max-scale))
(set-val saved-scale max-scale)
(reset! pan-x-enabled? (> (get-val scale) x-threshold-scale))
(reset! pan-y-enabled? (> (get-val scale) y-threshold-scale)))
(set-val scale (timing c/max-scale))
(set-val saved-scale c/max-scale))
:else
(do
(set-val saved-scale (get-val scale))
(set-val pinch-x-start (get-val pinch-x))
(set-val pinch-y-start (get-val pinch-y))
(reset! pan-x-enabled? (> (get-val scale) x-threshold-scale))
(reset! pan-y-enabled? (> (get-val scale) y-threshold-scale))))))))
(when (< (get-val scale) x-threshold-scale)
(center-x animations false))
(when (< (get-val scale) y-threshold-scale)
(center-y animations false))))))
(gesture/on-finalize
(fn []
(let [curr-offset-y (+ (get-val pan-y) (get-val pinch-y))
max-offset-y (utils/get-max-offset height screen-height (get-val scale))
max-offset-y (if (neg? curr-offset-y) (- max-offset-y) max-offset-y)
curr-offset-x (+ (get-val pan-x) (get-val pinch-x))
max-offset-x (utils/get-max-offset width screen-width (get-val scale))
max-offset-x (if (neg? curr-offset-x) (- max-offset-x) max-offset-x)]
(when (and (> (get-val scale) y-threshold-scale)
(> (Math/abs curr-offset-y) (Math/abs max-offset-y)))
(set-val pinch-y (timing c/init-offset))
(set-val pinch-y-start c/init-offset)
(set-val pan-y (timing max-offset-y))
(set-val pan-y-start max-offset-y))
(when (and (> (get-val scale) x-threshold-scale)
(> (Math/abs curr-offset-x) (Math/abs max-offset-x)))
(set-val pinch-x (timing c/init-offset))
(set-val pinch-x-start c/init-offset)
(set-val pan-x (timing max-offset-x))
(set-val pan-x-start max-offset-x))
(reset! pan-x-enabled? (> (get-val scale) x-threshold-scale))
(reset! pan-y-enabled? (> (get-val scale) y-threshold-scale)))))))
(defn pan-x-gesture
[{:keys [width]}
[{:keys [width screen-width x-threshold-scale]}
{:keys [scale pan-x-start pan-x pinch-x pinch-x-start]}
{:keys [pan-x-enabled? x-threshold-scale]}
{:keys [pan-x-enabled?]}
rescale]
(->
(gesture/gesture-pan)
@ -162,17 +217,17 @@
(gesture/on-end
(fn [e]
(let [curr-offset (+ (get-val pan-x) (get-val pinch-x-start))
max-offset (get-max-offset width width (get-val scale))
max-offset (utils/get-max-offset width screen-width (get-val scale))
max-offset (if (neg? curr-offset) (- max-offset) max-offset)]
(cond
(< (get-val scale) x-threshold-scale)
(rescale min-scale)
(rescale c/min-scale)
(> (Math/abs curr-offset) (Math/abs max-offset))
(do
(set-val pan-x (timing max-offset))
(set-val pan-x-start max-offset)
(set-val pinch-x (timing init-offset))
(set-val pinch-x-start init-offset))
(set-val pinch-x (timing c/init-offset))
(set-val pinch-x-start c/init-offset))
:else
(let [lower-bound (- (- (Math/abs max-offset)) (get-val pinch-x-start))
upper-bound (- (Math/abs max-offset) (get-val pinch-x-start))]
@ -182,9 +237,9 @@
(defn pan-y-gesture
[{:keys [height screen-height]}
[{:keys [height screen-height y-threshold-scale]}
{:keys [scale pan-y-start pan-y pinch-y pinch-y-start]}
{:keys [pan-y-enabled? y-threshold-scale]}
{:keys [pan-y-enabled?]}
rescale]
(->
(gesture/gesture-pan)
@ -195,17 +250,17 @@
(gesture/on-end
(fn [e]
(let [curr-offset (+ (get-val pan-y) (get-val pinch-y-start))
max-offset (get-max-offset height screen-height (get-val scale))
max-offset (utils/get-max-offset height screen-height (get-val scale))
max-offset (if (neg? curr-offset) (- max-offset) max-offset)]
(cond
(< (get-val scale) y-threshold-scale)
(rescale min-scale)
(rescale c/min-scale)
(> (Math/abs curr-offset) (Math/abs max-offset))
(do
(set-val pan-y (timing max-offset))
(set-val pan-y-start max-offset)
(set-val pinch-y (timing init-offset))
(set-val pinch-y-start init-offset))
(set-val pinch-y (timing c/init-offset))
(set-val pinch-y-start c/init-offset))
:else
(let [lower-bound (- (- (Math/abs max-offset)) (get-val pinch-y-start))
upper-bound (- (Math/abs max-offset) (get-val pinch-y-start))]
@ -213,91 +268,49 @@
(set-val-decay pan-y (oget e "velocityY") [lower-bound upper-bound])
(set-val-decay pan-y-start (oget e "velocityY") [lower-bound upper-bound]))))))))
(defn reset-values
[exit?
{:keys [pan-x pan-x-start pan-y pan-y-start pinch-x pinch-y pinch-x-start pinch-y-start]}
{:keys [focal-x focal-y]}]
(let [duration (if exit? 100 default-duration)]
(set-val pan-x (timing init-offset duration))
(set-val pinch-x (timing init-offset duration))
(set-val pan-x-start init-offset)
(set-val pinch-x-start init-offset)
(set-val pan-y (timing init-offset duration))
(set-val pinch-y (timing init-offset duration))
(set-val pinch-y-start init-offset)
(set-val pan-y-start init-offset)
(reset! focal-x nil)
(reset! focal-y nil)))
(defn rescale-image
[value
exit?
{:keys [scale saved-scale] :as animations}
{:keys [pan-x-enabled? pan-y-enabled? x-threshold-scale y-threshold-scale] :as props}]
(set-val scale (timing value (if exit? 100 300)))
(set-val saved-scale value)
(when (= value min-scale)
(reset-values exit? animations props))
(reset! pan-x-enabled? (> value x-threshold-scale))
(reset! pan-y-enabled? (> value y-threshold-scale)))
;;; 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
(defn handle-exit-lightbox-signal
[exit-lightbox-signal index scale rescale]
(when (= exit-lightbox-signal index)
(if (> scale min-scale)
(do
(rescale min-scale true)
(js/setTimeout #(rf/dispatch [:navigate-back]) 70))
(rf/dispatch [:navigate-back]))
(js/setTimeout #(rf/dispatch [:chat.ui/exit-lightbox-signal nil]) 500)))
(defn handle-zoom-out-signal
[zoom-out-signal index scale rescale]
(when (and (= zoom-out-signal index) (> scale min-scale))
(rescale min-scale true)))
;;;; Finally, the component
(defn zoomable-image
[{:keys [image-width image-height content message-id]} index border-value on-tap]
[{:keys [image-width image-height content message-id]} index border-radius on-tap]
[:f>
(fn []
(let [shared-element-id (rf/sub [:shared-element-id])
exit-lightbox-signal (rf/sub [:lightbox/exit-signal])
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
width (:width (rn/get-window))
height (* image-height (/ (:width (rn/get-window)) image-width))
screen-height (:height (rn/get-window))
dimensions {:width width
:height height
:screen-height screen-height}
animations {:scale (use-val min-scale)
:saved-scale (use-val min-scale)
:pan-x-start (use-val init-offset)
:pan-x (use-val init-offset)
:pan-y-start (use-val init-offset)
:pan-y (use-val init-offset)
:pinch-x-start (use-val init-offset)
:pinch-x (use-val init-offset)
:pinch-y-start (use-val init-offset)
:pinch-y (use-val init-offset)
curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
focused? (= shared-element-id message-id)
dimensions (utils/get-dimensions image-width image-height curr-orientation)
animations {:scale (use-val c/min-scale)
:saved-scale (use-val c/min-scale)
:pan-x-start (use-val c/init-offset)
:pan-x (use-val c/init-offset)
:pan-y-start (use-val c/init-offset)
:pan-y (use-val c/init-offset)
:pinch-x-start (use-val c/init-offset)
:pinch-x (use-val c/init-offset)
:pinch-y-start (use-val c/init-offset)
:pinch-y (use-val c/init-offset)
:pinch-x-max (use-val js/Infinity)
:pinch-y-max (use-val js/Infinity)}
props {:x-threshold-scale 1
:y-threshold-scale (/ screen-height (min screen-height height))
:pan-x-enabled? (reagent/atom false)
:pan-y-enabled? (reagent/atom false)
:focal-x (reagent/atom nil)
:focal-y (reagent/atom nil)}
:pinch-y-max (use-val js/Infinity)
:rotate (use-val c/init-rotation)
:rotate-scale (use-val c/min-scale)}
props {:pan-x-enabled? (reagent/atom false)
:pan-y-enabled? (reagent/atom false)
:focal-x (reagent/atom nil)
:focal-y (reagent/atom nil)}
rescale (fn [value exit?]
(rescale-image value exit? animations props))]
(handle-exit-lightbox-signal exit-lightbox-signal index (get-val (:scale animations)) rescale)
(handle-zoom-out-signal zoom-out-signal index (get-val (:scale animations)) rescale)
(rescale-image value exit? dimensions animations props))]
(when platform/ios?
(handle-orientation-change curr-orientation focused? dimensions animations props)
(utils/handle-exit-lightbox-signal exit-lightbox-signal
index
(get-val (:scale animations))
rescale))
(utils/handle-zoom-out-signal zoom-out-signal index (get-val (:scale animations)) rescale)
[:f>
(fn []
(let [tap (tap-gesture on-tap)
double-tap (double-tap-gesture dimensions animations props rescale)
double-tap (double-tap-gesture dimensions animations rescale)
pinch (pinch-gesture dimensions animations props rescale)
pan-x (pan-x-gesture dimensions animations props rescale)
pan-y (pan-y-gesture dimensions animations props rescale)
@ -305,16 +318,9 @@
(gesture/simultaneous pinch pan-x pan-y)
(gesture/exclusive double-tap tap))]
[gesture/gesture-detector {:gesture composed-gestures}
[reanimated/fast-image
{:source {:uri (:image content)}
:native-ID (when (= shared-element-id message-id) :shared-element)
:style (reanimated/apply-animations-to-style
{:transform [{:translateX (:pan-x animations)}
{:translateY (:pan-y animations)}
{:translateX (:pinch-x animations)}
{:translateY (:pinch-y animations)}
{:scale (:scale animations)}]
:border-radius border-value}
{:width width
:height height})}]]))]))])
[reanimated/view
{:style (style/container dimensions animations)}
[reanimated/fast-image
{:source {:uri (:image content)}
:native-ID (when focused? :shared-element)
:style (style/image dimensions animations border-radius)}]]]))]))])

View File

@ -117,6 +117,7 @@
(reg-root-key-sub :chats-home-list :chats-home-list)
(reg-root-key-sub :lightbox/exit-signal :lightbox/exit-signal)
(reg-root-key-sub :lightbox/zoom-out-signal :lightbox/zoom-out-signal)
(reg-root-key-sub :lightbox/orientation :lightbox/orientation)
;;messages
(reg-root-key-sub :messages/messages :messages)

View File

@ -9559,6 +9559,11 @@ react-native-navigation@^7.27.1:
react-lifecycles-compat "2.0.0"
tslib "1.9.3"
react-native-orientation-locker@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/react-native-orientation-locker/-/react-native-orientation-locker-1.5.0.tgz#324853937eed4835ecd1c8613ab2206135d908ac"
integrity sha512-4XOCGmNN4BXg5JUFjUuXpsfhPJmbA3LkQilJO1ed+6vL97teTdG2w5IEevKiqL9/hPjeWE8YYtX/YW+yp53hkg==
react-native-permissions@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-2.1.5.tgz#6cfc4f1ab1590f4952299b7cdc9698525ad540e0"