feat: drag to dismiss lightbox (#15349)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]
[react-native.safe-area :as safe-area]
[status-im2.contexts.chat.messages.content.album.style :as style]
[status-im2.constants :as constants]
[status-im2.contexts.chat.messages.content.image.view :as image]
@ -19,57 +20,63 @@
(defn album-message
[{:keys [albumize?] :as message} context on-long-press]
(let [shared-element-id (rf/sub [:shared-element-id])
first-image (first (:album message))
album-style (if (> (:image-width first-image) (:image-height first-image))
:landscape
:portrait)
images-count (count (:album message))
;; album images are always square, except when we have 3 images, then they must be rectangular
;; (portrait or landscape)
portrait? (and (= images-count rectangular-style-count) (= album-style :portrait))
text (:text (:content first-image))]
(if (and albumize? (> images-count 1))
[:<>
(when (not= text "placeholder")
[rn/view {:style {:margin-bottom 10}} [text/text-content first-image context]])
[rn/view
{:style (style/album-container portrait?)}
(map-indexed
(fn [index item]
(let [images-size-key (if (< images-count constants/max-album-photos) images-count :default)
size (get-in constants/album-image-sizes [images-size-key index])
dimensions (if (not= images-count rectangular-style-count)
{:width size :height size}
(find-size size album-style))]
[rn/touchable-opacity
{:key (:message-id item)
:active-opacity 1
:on-long-press #(on-long-press message context)
:on-press (fn []
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)])
(js/setTimeout #(rf/dispatch [:navigate-to :lightbox
{:messages (:album message)
:index index}])
100))}
[fast-image/fast-image
{:style (style/image dimensions index portrait? images-count)
:source {:uri (:image (:content item))}
:native-ID (when (and (= shared-element-id (:message-id item))
(< index constants/max-album-photos))
:shared-element)}]
(when (and (> images-count constants/max-album-photos)
(= index (- constants/max-album-photos 1)))
[rn/view
{:style style/overlay}
[quo/text
{:weight :bold
:size :heading-2
:style {:color colors/white}}
(str "+" (- images-count (dec constants/max-album-photos)))]])]))
(:album message))]]
[:<>
(map-indexed
(fn [index item]
[image/image-message index item context #(on-long-press message context)])
(:album message))])))
[:f>
(fn []
(let [insets (safe-area/use-safe-area)
shared-element-id (rf/sub [:shared-element-id])
first-image (first (:album message))
album-style (if (> (:image-width first-image) (:image-height first-image))
:landscape
:portrait)
images-count (count (:album message))
;; album images are always square, except when we have 3 images, then they must be rectangular
;; (portrait or landscape)
portrait? (and (= images-count rectangular-style-count) (= album-style :portrait))
text (:text (:content first-image))]
(if (and albumize? (> images-count 1))
[:<>
(when (not= text "placeholder")
[rn/view {:style {:margin-bottom 10}} [text/text-content first-image context]])
[rn/view
{:style (style/album-container portrait?)}
(map-indexed
(fn [index item]
(let [images-size-key (if (< images-count constants/max-album-photos)
images-count
:default)
size (get-in constants/album-image-sizes [images-size-key index])
dimensions (if (not= images-count rectangular-style-count)
{:width size :height size}
(find-size size album-style))]
[rn/touchable-opacity
{:key (:message-id item)
:active-opacity 1
:on-long-press #(on-long-press message context)
:on-press (fn []
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)])
(js/setTimeout #(rf/dispatch [:navigate-to :lightbox
{:messages (:album message)
:index index
:insets insets}])
100))}
[fast-image/fast-image
{:style (style/image dimensions index portrait? images-count)
:source {:uri (:image (:content item))}
:native-ID (when (and (= shared-element-id (:message-id item))
(< index constants/max-album-photos))
:shared-element)}]
(when (and (> images-count constants/max-album-photos)
(= index (- constants/max-album-photos 1)))
[rn/view
{:style style/overlay}
[quo/text
{:weight :bold
:size :heading-2
:style {:color colors/white}}
(str "+" (- images-count (dec constants/max-album-photos)))]])]))
(:album message))]]
[:<>
(map-indexed
(fn [index item]
[image/image-message index item context #(on-long-press message context)])
(:album message))])))])

View File

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

View File

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

View File

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