From 89234089721f77638d74bc9503bbbdef5821d9d6 Mon Sep 17 00:00:00 2001 From: Omar Basem Date: Fri, 3 Mar 2023 16:33:28 +0400 Subject: [PATCH] Landscape Mode (#15175) * feat: landscape mode --- package.json | 1 + src/mocks/js_dependencies.cljs | 5 + src/react_native/gesture.cljs | 2 + src/react_native/orientation.cljs | 32 ++ src/react_native/reanimated.cljs | 16 +- src/status_im2/contexts/chat/events.cljs | 6 + .../contexts/chat/lightbox/bottom_view.cljs | 78 ++++ .../contexts/chat/lightbox/common.cljs | 14 + .../contexts/chat/lightbox/style.cljs | 58 ++- .../contexts/chat/lightbox/top_view.cljs | 75 ++++ .../contexts/chat/lightbox/view.cljs | 269 +++++++------- .../lightbox/zoomable_image/constants.cljs | 15 + .../chat/lightbox/zoomable_image/style.cljs | 27 ++ .../chat/lightbox/zoomable_image/utils.cljs | 83 +++++ .../chat/lightbox/zoomable_image/view.cljs | 338 +++++++++--------- src/status_im2/subs/root.cljs | 1 + yarn.lock | 5 + 17 files changed, 696 insertions(+), 329 deletions(-) create mode 100644 src/react_native/orientation.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/bottom_view.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/common.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/top_view.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/zoomable_image/constants.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs create mode 100644 src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs diff --git a/package.json b/package.json index 0df7010bcc..5f16cd1c85 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index 0c0ed72e7e..872afd9feb 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -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 diff --git a/src/react_native/gesture.cljs b/src/react_native/gesture.cljs index 6428b0e422..000d59f5ad 100644 --- a/src/react_native/gesture.cljs +++ b/src/react_native/gesture.cljs @@ -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?)) diff --git a/src/react_native/orientation.cljs b/src/react_native/orientation.cljs new file mode 100644 index 0000000000..78d5f27922 --- /dev/null +++ b/src/react_native/orientation.cljs @@ -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") diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index 794bbebbf4..78dc29b4f6 100644 --- a/src/react_native/reanimated.cljs +++ b/src/react_native/reanimated.cljs @@ -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 diff --git a/src/status_im2/contexts/chat/events.cljs b/src/status_im2/contexts/chat/events.cljs index 9e8d29f9c8..ca0bab4a66 100644 --- a/src/status_im2/contexts/chat/events.cljs +++ b/src/status_im2/contexts/chat/events.cljs @@ -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)}) + diff --git a/src/status_im2/contexts/chat/lightbox/bottom_view.cljs b/src/status_im2/contexts/chat/lightbox/bottom_view.cljs new file mode 100644 index 0000000000..f5f46d09c8 --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/bottom_view.cljs @@ -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)}]]))]) diff --git a/src/status_im2/contexts/chat/lightbox/common.cljs b/src/status_im2/contexts/chat/lightbox/common.cljs new file mode 100644 index 0000000000..cf6283ad50 --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/common.cljs @@ -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)) diff --git a/src/status_im2/contexts/chat/lightbox/style.cljs b/src/status_im2/contexts/chat/lightbox/style.cljs index d2f388277c..ddb92de6bc 100644 --- a/src/status_im2/contexts/chat/lightbox/style.cljs +++ b/src/status_im2/contexts/chat/lightbox/style.cljs @@ -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 diff --git a/src/status_im2/contexts/chat/lightbox/top_view.cljs b/src/status_im2/contexts/chat/lightbox/top_view.cljs new file mode 100644 index 0000000000..66988fedcc --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/top_view.cljs @@ -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}]]]]))]) diff --git a/src/status_im2/contexts/chat/lightbox/view.cljs b/src/status_im2/contexts/chat/lightbox/view.cljs index 26b5ffb9b7..3a407bb562 100644 --- a/src/status_im2/contexts/chat/lightbox/view.cljs +++ b/src/status_im2/contexts/chat/lightbox/view.cljs @@ -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])]))]))]) diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/constants.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/constants.cljs new file mode 100644 index 0000000000..9401caa803 --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/constants.cljs @@ -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) diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs new file mode 100644 index 0000000000..9206886a2c --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/style.cljs @@ -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})) diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs new file mode 100644 index 0000000000..95b9020f52 --- /dev/null +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/utils.cljs @@ -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)) diff --git a/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs b/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs index 2f302700d8..fa0ae2dcb6 100644 --- a/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs +++ b/src/status_im2/contexts/chat/lightbox/zoomable_image/view.cljs @@ -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)}]]]))]))]) diff --git a/src/status_im2/subs/root.cljs b/src/status_im2/subs/root.cljs index 0b9fa2cf90..fba978fdcf 100644 --- a/src/status_im2/subs/root.cljs +++ b/src/status_im2/subs/root.cljs @@ -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) diff --git a/yarn.lock b/yarn.lock index e3847c18d4..e7c1e22a13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"