parent
52b87bab3e
commit
8923408972
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?))
|
||||
|
|
|
@ -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")
|
|
@ -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
|
||||
|
|
|
@ -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)})
|
||||
|
||||
|
|
|
@ -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)}]]))])
|
|
@ -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))
|
|
@ -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}
|
||||
(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
|
||||
:left 20
|
||||
:top (+ 12 top-inset)
|
||||
:padding-horizontal 20
|
||||
:top (if platform/ios? top-inset 0)
|
||||
:height common/top-view-height
|
||||
:z-index 4
|
||||
:flex-direction :row
|
||||
:width "100%"}))
|
||||
|
||||
: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
|
||||
|
|
|
@ -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}]]]]))])
|
|
@ -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]
|
||||
[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))])))
|
||||
(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
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
:width (+ width seperator-width)
|
||||
:height height
|
||||
:align-items :center
|
||||
:justify-content :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]
|
||||
(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 (first messages) insets opacity-value scroll-index])
|
||||
[top-view/top-view (first messages) insets scroll-index animations landscape?
|
||||
screen-width])
|
||||
[rn/flat-list
|
||||
{:ref #(reset! flat-list-ref %)
|
||||
{:ref #(reset! (:flat-list-ref atoms) %)
|
||||
:key-fn :message-id
|
||||
:style {:width (+ window-width 16)}
|
||||
:style {:width (+ screen-width seperator-width)}
|
||||
:data @data
|
||||
:render-fn image
|
||||
:render-data {:opacity-value opacity-value
|
||||
:border-value border-value
|
||||
:transparent? transparent?}
|
||||
:horizontal true
|
||||
: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 get-item-layout
|
||||
:viewability-config {:view-area-coverage-percent-threshold 50}
|
||||
: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-not @transparent?
|
||||
[bottom-view messages index scroll-index insets opacity-value])])]))])
|
||||
(when (and (not @transparent?) (not landscape?))
|
||||
[bottom-view/bottom-view messages index scroll-index insets animations
|
||||
item-width atoms])]))]))])
|
||||
|
|
|
@ -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)
|
|
@ -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}))
|
|
@ -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))
|
|
@ -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)
|
||||
(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 (> double-tap-scale y-threshold-scale)
|
||||
(when (> c/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))))))
|
||||
(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))
|
||||
(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))))))))
|
||||
(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)
|
||||
: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/view
|
||||
{:style (style/container dimensions animations)}
|
||||
[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})}]]))]))])
|
||||
|
||||
:native-ID (when focused? :shared-element)
|
||||
:style (style/image dimensions animations border-radius)}]]]))]))])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue