|
@ -7,6 +7,7 @@
|
|||
|
||||
# Xcode
|
||||
#
|
||||
/ios/.xcode.env.local
|
||||
/component-spec
|
||||
result/
|
||||
build/
|
||||
|
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 869 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 843 B After Width: | Height: | Size: 946 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -76,6 +76,7 @@
|
|||
(def neutral-90-opa-0 (alpha neutral-90 0))
|
||||
|
||||
;;95 with transparency
|
||||
(def neutral-95-opa-0 (alpha neutral-95 0))
|
||||
(def neutral-95-opa-60 (alpha neutral-95 0.6))
|
||||
(def neutral-95-opa-70 (alpha neutral-95 0.7))
|
||||
(def neutral-95-opa-80 (alpha neutral-95 0.8))
|
||||
|
@ -84,6 +85,7 @@
|
|||
|
||||
;;100 with transparency
|
||||
(def neutral-100-opa-0 (alpha neutral-100 0))
|
||||
(def neutral-100-opa-5 (alpha neutral-100 0.05))
|
||||
(def neutral-100-opa-10 (alpha neutral-100 0.1))
|
||||
(def neutral-100-opa-30 (alpha neutral-100 0.3))
|
||||
(def neutral-100-opa-60 (alpha neutral-100 0.6))
|
||||
|
|
|
@ -171,6 +171,15 @@
|
|||
(with-decay (clj->js {:velocity velocity
|
||||
:clamp clamp}))))
|
||||
|
||||
(defn animate
|
||||
([animation value]
|
||||
(animate animation value default-duration))
|
||||
([animation value duration]
|
||||
(set-shared-value animation
|
||||
(with-timing value
|
||||
(clj->js {:duration duration
|
||||
:easing (default-easing)})))))
|
||||
|
||||
(defn with-timing-duration
|
||||
[val duration]
|
||||
(with-timing val
|
||||
|
|
|
@ -39,6 +39,24 @@
|
|||
(let [current-chat-id (or chat-id (:current-chat-id db))]
|
||||
{:db (assoc-in db [:chat/inputs current-chat-id :input-text] (text->emoji new-input))}))
|
||||
|
||||
(rf/defn set-input-content-height
|
||||
{:events [:chat.ui/set-input-content-height]}
|
||||
[{db :db} content-height chat-id]
|
||||
(let [current-chat-id (or chat-id (:current-chat-id db))]
|
||||
{:db (assoc-in db [:chat/inputs current-chat-id :input-content-height] content-height)}))
|
||||
|
||||
(rf/defn set-input-maximized
|
||||
{:events [:chat.ui/set-input-maximized]}
|
||||
[{db :db} maximized? chat-id]
|
||||
(let [current-chat-id (or chat-id (:current-chat-id db))]
|
||||
{:db (assoc-in db [:chat/inputs current-chat-id :input-maximized?] maximized?)}))
|
||||
|
||||
(rf/defn set-input-refocus
|
||||
{:events [:chat.ui/set-input-refocus]}
|
||||
[{db :db} refocus? chat-id]
|
||||
(let [current-chat-id (or chat-id (:current-chat-id db))]
|
||||
{:db (assoc-in db [:chat/inputs current-chat-id :input-refocus?] refocus?)}))
|
||||
|
||||
(rf/defn select-mention
|
||||
{:events [:chat.ui/select-mention]}
|
||||
[{:keys [db] :as cofx} text-input-ref {:keys [primary-name searched-text match public-key] :as user}]
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.actions.style
|
||||
(:require
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
|
||||
(def actions-container
|
||||
{:height constants/actions-container-height
|
||||
:justify-content :space-between
|
||||
:align-items :center
|
||||
:z-index 2
|
||||
:flex-direction :row})
|
||||
|
||||
(defn send-button
|
||||
[opacity z-index]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:z-index z-index
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-95)}))
|
|
@ -0,0 +1,148 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.actions.view
|
||||
(:require
|
||||
[quo2.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[react-native.permissions :as permissions]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.common.alert.events :as alert]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.messages.list.view :as messages.list]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.actions.style :as style]))
|
||||
|
||||
(defn send-message
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value focused? maximized?]}
|
||||
{:keys [height saved-height last-height opacity background-y container-opacity]}
|
||||
window-height]
|
||||
(reanimated/animate height constants/input-height)
|
||||
(reanimated/set-shared-value saved-height constants/input-height)
|
||||
(reanimated/set-shared-value last-height constants/input-height)
|
||||
(reanimated/animate opacity 0)
|
||||
(when-not @focused?
|
||||
(js/setTimeout #(reanimated/animate container-opacity constants/empty-opacity) 300))
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y
|
||||
(- window-height))
|
||||
300)
|
||||
(rf/dispatch [:chat.ui/send-current-message])
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height])
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text nil])
|
||||
(reset! maximized? false)
|
||||
(reset! text-value "")
|
||||
(when @input-ref
|
||||
(.clear ^js @input-ref))
|
||||
(messages.list/scroll-to-bottom))
|
||||
|
||||
(defn send-button
|
||||
[props
|
||||
{:keys [text-value] :as state}
|
||||
animations
|
||||
window-height
|
||||
images?]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [btn-opacity (reanimated/use-shared-value 0)
|
||||
z-index (reagent/atom 0)]
|
||||
[:f>
|
||||
(fn []
|
||||
(rn/use-effect (fn []
|
||||
(if (or (not-empty @text-value) images?)
|
||||
(when-not (= @z-index 1)
|
||||
(reset! z-index 1)
|
||||
(js/setTimeout #(reanimated/animate btn-opacity 1) 50))
|
||||
(when-not (= @z-index 0)
|
||||
(reanimated/animate btn-opacity 0)
|
||||
(js/setTimeout #(reset! z-index 0) 300))))
|
||||
[(and (empty? @text-value) (not images?))])
|
||||
[reanimated/view
|
||||
{:style (style/send-button btn-opacity @z-index)}
|
||||
[quo/button
|
||||
{:icon true
|
||||
:size 32
|
||||
:accessibility-label :send-message-button
|
||||
:on-press #(send-message props state animations window-height)}
|
||||
:i/arrow-up]])]))])
|
||||
|
||||
(defn audio-button
|
||||
[]
|
||||
[quo/button
|
||||
{:on-press #(js/alert "to be added")
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32}
|
||||
:i/audio])
|
||||
|
||||
(defn camera-button
|
||||
[]
|
||||
[quo/button
|
||||
{:on-press #(js/alert "to be implemented")
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32
|
||||
:style {:margin-right 12}}
|
||||
:i/camera])
|
||||
|
||||
(defn open-photo-selector
|
||||
[{:keys [input-ref]}
|
||||
{:keys [focused?]}
|
||||
{:keys [height]}
|
||||
insets]
|
||||
(permissions/request-permissions
|
||||
{:permissions [:read-external-storage :write-external-storage]
|
||||
:on-allowed (fn []
|
||||
(when platform/android?
|
||||
(when @focused?
|
||||
(rf/dispatch [:chat.ui/set-input-refocus true]))
|
||||
(when @input-ref
|
||||
(.blur ^js @input-ref)))
|
||||
(rf/dispatch [:chat.ui/set-input-content-height
|
||||
(reanimated/get-shared-value height)])
|
||||
(rf/dispatch [:open-modal :photo-selector {:insets insets}]))
|
||||
:on-denied (fn []
|
||||
(alert/show-popup (i18n/label :t/error)
|
||||
(i18n/label
|
||||
:t/external-storage-denied)))}))
|
||||
|
||||
(defn image-button
|
||||
[props state animations insets]
|
||||
[quo/button
|
||||
{:on-press #(open-photo-selector props state animations insets)
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32
|
||||
:style {:margin-right 12}}
|
||||
:i/image])
|
||||
|
||||
(defn reaction-button
|
||||
[]
|
||||
[quo/button
|
||||
{:on-press #(js/alert "to be implemented")
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32
|
||||
:style {:margin-right 12}}
|
||||
:i/reaction])
|
||||
|
||||
(defn format-button
|
||||
[]
|
||||
[quo/button
|
||||
{:on-press #(js/alert "to be implemented")
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32}
|
||||
:i/format])
|
||||
|
||||
(defn view
|
||||
[props state animations window-height insets images?]
|
||||
[rn/view {:style style/actions-container}
|
||||
[rn/view {:style {:flex-direction :row}}
|
||||
[camera-button]
|
||||
[image-button props state animations insets]
|
||||
[reaction-button]
|
||||
[format-button]]
|
||||
[send-button props state animations window-height images?]
|
||||
[audio-button]])
|
|
@ -0,0 +1,30 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.constants
|
||||
(:require
|
||||
[quo2.foundations.typography :as typography]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
(def ^:const bar-container-height 20)
|
||||
|
||||
(def ^:const input-height (if platform/ios? 32 42))
|
||||
|
||||
(def ^:const actions-container-height 56)
|
||||
|
||||
(def ^:const composer-default-height (+ bar-container-height input-height actions-container-height))
|
||||
|
||||
(def ^:const multiline-minimized-height (+ input-height 18))
|
||||
|
||||
(def ^:const empty-opacity 0.7)
|
||||
|
||||
(def ^:const images-container-height 76)
|
||||
|
||||
(def ^:const extra-content-offset (if platform/ios? 6 0))
|
||||
|
||||
(def ^:const content-change-threshold 10)
|
||||
|
||||
(def ^:const drag-threshold 30)
|
||||
|
||||
(def ^:const velocity-threshold -1000)
|
||||
|
||||
(def ^:const background-threshold 0.75)
|
||||
|
||||
(def ^:const line-height (:line-height typography/paragraph-1))
|
|
@ -0,0 +1,92 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.effects
|
||||
(:require
|
||||
[status-im.async-storage.core :as async-storage]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.number :as utils.number]))
|
||||
|
||||
(defn reenter-screen-effect
|
||||
[{:keys [text-value saved-cursor-position maximized?]}
|
||||
{:keys [content-height]}
|
||||
{:keys [input-content-height input-text input-maximized?]}]
|
||||
(when (and (empty? @text-value) (not= input-text nil))
|
||||
(reset! text-value input-text)
|
||||
(reset! content-height input-content-height)
|
||||
(reset! saved-cursor-position (count input-text)))
|
||||
(when input-maximized?
|
||||
(reset! maximized? true)))
|
||||
|
||||
(defn maximized-effect
|
||||
[{:keys [maximized?]}
|
||||
{:keys [height saved-height last-height]}
|
||||
{:keys [max-height]}
|
||||
{:keys [input-content-height]}]
|
||||
(when (or @maximized? (>= input-content-height max-height))
|
||||
(reanimated/animate height max-height)
|
||||
(reanimated/set-shared-value saved-height max-height)
|
||||
(reanimated/set-shared-value last-height max-height)))
|
||||
|
||||
(defn refocus-effect
|
||||
[{:keys [input-ref]}
|
||||
{:keys [input-refocus?]}]
|
||||
(when (and input-refocus? @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
(rf/dispatch [:chat.ui/set-input-refocus false])))
|
||||
|
||||
(defn layout-effect
|
||||
[{:keys [lock-layout?]}]
|
||||
(when-not @lock-layout?
|
||||
(js/setTimeout #(reset! lock-layout? true) 500)))
|
||||
|
||||
(defn kb-default-height-effect
|
||||
[{:keys [kb-default-height]}]
|
||||
(when-not @kb-default-height
|
||||
(async-storage/get-item :kb-default-height
|
||||
#(reset! kb-default-height (utils.number/parse-int % nil)))))
|
||||
|
||||
(defn background-effect
|
||||
[{:keys [maximized?]}
|
||||
{:keys [opacity background-y]}
|
||||
{:keys [max-height]}
|
||||
{:keys [input-content-height]}]
|
||||
(when (or @maximized? (>= input-content-height (* max-height constants/background-threshold)))
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1)))
|
||||
|
||||
(defn images-effect
|
||||
[{:keys [container-opacity]}
|
||||
images?]
|
||||
(when images?
|
||||
(reanimated/animate container-opacity 1)))
|
||||
|
||||
(defn empty-effect
|
||||
[{:keys [text-value maximized? focused?]}
|
||||
{:keys [container-opacity]}
|
||||
images?]
|
||||
(when (and (empty? @text-value) (not images?) (not @maximized?) (not @focused?))
|
||||
(reanimated/animate-delay container-opacity constants/empty-opacity 200)))
|
||||
|
||||
(defn component-will-unmount
|
||||
[{:keys [keyboard-show-listener keyboard-hide-listener keyboard-frame-listener]}]
|
||||
(.remove ^js @keyboard-show-listener)
|
||||
(.remove ^js @keyboard-hide-listener)
|
||||
(.remove ^js @keyboard-frame-listener))
|
||||
|
||||
(defn initialize
|
||||
[props state animations {:keys [max-height] :as dimensions} chat-input keyboard-height images?]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(maximized-effect state animations dimensions chat-input)
|
||||
(refocus-effect props chat-input)
|
||||
(reenter-screen-effect state dimensions chat-input)
|
||||
(layout-effect state)
|
||||
(kb-default-height-effect state)
|
||||
(background-effect state animations dimensions chat-input)
|
||||
(images-effect animations images?)
|
||||
(empty-effect state animations images?)
|
||||
(kb/add-kb-listeners props state animations dimensions keyboard-height)
|
||||
#(component-will-unmount props))
|
||||
[max-height]))
|
|
@ -0,0 +1,103 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.gesture
|
||||
(:require
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[oops.core :as oops]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn set-opacity
|
||||
[velocity opacity translation expanding? min-height max-height new-height saved-height]
|
||||
(let [remaining-height (if expanding?
|
||||
(- max-height (reanimated/get-shared-value saved-height))
|
||||
(- (reanimated/get-shared-value saved-height) min-height))
|
||||
progress (if (= new-height min-height) 1 (/ translation remaining-height))
|
||||
currently-expanding? (neg? velocity)
|
||||
max-opacity? (and currently-expanding? (= (reanimated/get-shared-value opacity) 1))
|
||||
min-opacity? (and (not currently-expanding?)
|
||||
(= (reanimated/get-shared-value opacity) 0))]
|
||||
(if (>= translation 0)
|
||||
(when (and (not expanding?) (not min-opacity?))
|
||||
(reanimated/set-shared-value opacity (- 1 progress)))
|
||||
(when (and expanding? (not max-opacity?))
|
||||
(reanimated/set-shared-value opacity (Math/abs progress))))))
|
||||
|
||||
(defn maximize
|
||||
[{:keys [maximized?]}
|
||||
{:keys [height saved-height background-y opacity]}
|
||||
{:keys [max-height]}]
|
||||
(reanimated/animate height max-height)
|
||||
(reanimated/set-shared-value saved-height max-height)
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1)
|
||||
(reset! maximized? true)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized true]))
|
||||
|
||||
(defn minimize
|
||||
[{:keys [input-ref emoji-kb-extra-height saved-emoji-kb-extra-height]}]
|
||||
(when @emoji-kb-extra-height
|
||||
(reset! saved-emoji-kb-extra-height @emoji-kb-extra-height)
|
||||
(reset! emoji-kb-extra-height nil))
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])
|
||||
(when @input-ref
|
||||
(.blur ^js @input-ref)))
|
||||
|
||||
(defn bounce-back
|
||||
[{:keys [height saved-height opacity background-y]}
|
||||
{:keys [window-height]}
|
||||
starting-opacity]
|
||||
(reanimated/animate height (reanimated/get-shared-value saved-height))
|
||||
(when (zero? starting-opacity)
|
||||
(reanimated/animate opacity 0)
|
||||
(reanimated/animate-delay background-y (- window-height) 300)))
|
||||
|
||||
(defn drag-gesture
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [gesture-enabled?] :as state}
|
||||
{:keys [height saved-height last-height opacity background-y container-opacity] :as animations}
|
||||
{:keys [max-height lines] :as dimensions}
|
||||
keyboard-shown]
|
||||
(let [expanding? (atom true)
|
||||
starting-opacity (reanimated/get-shared-value opacity)]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/enabled @gesture-enabled?)
|
||||
(gesture/on-start (fn [event]
|
||||
(if-not keyboard-shown
|
||||
(do ; focus and end
|
||||
(when (< (oops/oget event "velocityY") constants/velocity-threshold)
|
||||
(reanimated/set-shared-value container-opacity 1)
|
||||
(reanimated/set-shared-value last-height max-height))
|
||||
(when @input-ref
|
||||
(.focus ^js @input-ref))
|
||||
(reset! gesture-enabled? false))
|
||||
(do ; else, will start gesture
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reset! expanding? (neg? (oops/oget event "velocityY")))))))
|
||||
(gesture/on-update (fn [event]
|
||||
(let [translation (oops/oget event "translationY")
|
||||
min-height (utils/get-min-height lines)
|
||||
new-height (- (reanimated/get-shared-value saved-height) translation)
|
||||
new-height (utils/bounded-val new-height min-height max-height)]
|
||||
(when keyboard-shown
|
||||
(reanimated/set-shared-value height new-height)
|
||||
(set-opacity (oops/oget event "velocityY")
|
||||
opacity
|
||||
translation
|
||||
@expanding?
|
||||
min-height
|
||||
max-height
|
||||
new-height
|
||||
saved-height)))))
|
||||
(gesture/on-end (fn []
|
||||
(let [diff (- (reanimated/get-shared-value height)
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
(if @gesture-enabled?
|
||||
(if (>= diff 0)
|
||||
(if (> diff constants/drag-threshold)
|
||||
(maximize state animations dimensions)
|
||||
(bounce-back animations dimensions starting-opacity))
|
||||
(if (> (Math/abs diff) constants/drag-threshold)
|
||||
(minimize props)
|
||||
(bounce-back animations dimensions starting-opacity)))
|
||||
(reset! gesture-enabled? true))))))))
|
|
@ -0,0 +1,42 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.gradients.style
|
||||
(:require
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.foundations.typography :as typography]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
|
||||
(defn top-gradient-style
|
||||
[opacity z-index]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
{:height 80
|
||||
:position :absolute
|
||||
:z-index z-index
|
||||
:top 0
|
||||
:left 0
|
||||
:right 0}))
|
||||
|
||||
(defn top-gradient
|
||||
[opacity z-index]
|
||||
{:colors [(colors/theme-colors colors/white-opa-0 colors/neutral-95-opa-0)
|
||||
(colors/theme-colors colors/white colors/neutral-95)]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (top-gradient-style opacity z-index)})
|
||||
|
||||
(def bottom-gradient-style
|
||||
{:height (if platform/ios? (:line-height typography/paragraph-1) 32)
|
||||
:position :absolute
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0
|
||||
:z-index 2})
|
||||
|
||||
(defn bottom-gradient
|
||||
[]
|
||||
{:colors [(colors/theme-colors colors/white colors/neutral-95)
|
||||
(colors/theme-colors colors/white-opa-0 colors/neutral-95-opa-0)]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style bottom-gradient-style})
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.gradients.view
|
||||
(:require
|
||||
[react-native.core :as rn]
|
||||
[react-native.linear-gradient :as linear-gradient]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.gradients.style :as style]))
|
||||
|
||||
|
||||
(defn view
|
||||
[{:keys [input-ref]}
|
||||
{:keys [gradient-z-index]}
|
||||
{:keys [gradient-opacity]}
|
||||
show-bottom-gradient?]
|
||||
[:f>
|
||||
(fn []
|
||||
[:<>
|
||||
[reanimated/linear-gradient (style/top-gradient gradient-opacity @gradient-z-index)]
|
||||
(when show-bottom-gradient?
|
||||
[rn/touchable-without-feedback
|
||||
{:on-press #(when @input-ref (.focus ^js @input-ref))
|
||||
:accessibility-label :bottom-gradient}
|
||||
[linear-gradient/linear-gradient (style/bottom-gradient)]])])])
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.handlers
|
||||
(:require [react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[oops.core :as oops]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn focus
|
||||
[{:keys [input-ref] :as props}
|
||||
{:keys [text-value focused? lock-selection? saved-cursor-position gradient-z-index]}
|
||||
{:keys [height saved-height last-height opacity background-y gradient-opacity container-opacity]
|
||||
:as animations}
|
||||
{:keys [max-height] :as dimensions}]
|
||||
(reset! focused? true)
|
||||
(reanimated/animate height (reanimated/get-shared-value last-height))
|
||||
(reanimated/set-shared-value saved-height (reanimated/get-shared-value last-height))
|
||||
(reanimated/animate container-opacity 1)
|
||||
(when (> (reanimated/get-shared-value last-height) (* constants/background-threshold max-height))
|
||||
(reanimated/animate opacity 1)
|
||||
(reanimated/set-shared-value background-y 0))
|
||||
(when (= @gradient-z-index -1)
|
||||
(reanimated/animate gradient-opacity 1)
|
||||
(reset! gradient-z-index 1))
|
||||
(js/setTimeout #(reset! lock-selection? false) 300)
|
||||
(when (and (not-empty @text-value) @input-ref)
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start @saved-cursor-position :end @saved-cursor-position}})))
|
||||
(kb/handle-refocus-emoji-kb-ios props animations dimensions))
|
||||
|
||||
(defn blur
|
||||
[{:keys [text-value focused? lock-selection? cursor-position saved-cursor-position gradient-z-index
|
||||
maximized?]}
|
||||
{:keys [height saved-height last-height gradient-opacity container-opacity opacity background-y]}
|
||||
{:keys [lines content-height max-height window-height]}
|
||||
images]
|
||||
(let [min-height (utils/get-min-height lines)
|
||||
reopen-height (utils/calc-reopen-height text-value min-height content-height saved-height)]
|
||||
(reset! focused? false)
|
||||
(reanimated/set-shared-value last-height reopen-height)
|
||||
(reanimated/animate height min-height)
|
||||
(reanimated/set-shared-value saved-height min-height)
|
||||
(reanimated/animate opacity 0)
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y (- window-height)) 300)
|
||||
(when (and (empty? @text-value) (empty? images))
|
||||
(reanimated/animate container-opacity constants/empty-opacity))
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(reset! lock-selection? true)
|
||||
(reset! saved-cursor-position @cursor-position)
|
||||
(reset! gradient-z-index (if (= (reanimated/get-shared-value gradient-opacity) 1) -1 0))
|
||||
(when (not= reopen-height max-height)
|
||||
(reset! maximized? false))))
|
||||
|
||||
(defn content-size-change
|
||||
[event
|
||||
{:keys [maximized?]}
|
||||
{:keys [height saved-height opacity background-y]}
|
||||
{:keys [content-height window-height max-height]}
|
||||
keyboard-shown]
|
||||
(when keyboard-shown
|
||||
(let [content-size (+ (oops/oget event "nativeEvent.contentSize.height")
|
||||
constants/extra-content-offset)
|
||||
new-height (utils/bounded-val content-size constants/input-height max-height)]
|
||||
(reset! content-height content-size)
|
||||
(when (utils/update-height? content-size height max-height maximized?)
|
||||
(reanimated/animate height new-height)
|
||||
(reanimated/set-shared-value saved-height new-height))
|
||||
(when (= new-height max-height)
|
||||
(reset! maximized? true))
|
||||
(if (utils/show-background? saved-height max-height new-height)
|
||||
(do
|
||||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1))
|
||||
(when (= (reanimated/get-shared-value opacity) 1)
|
||||
(reanimated/animate opacity 0)
|
||||
(js/setTimeout #(reanimated/set-shared-value background-y (- window-height)) 300)))
|
||||
(rf/dispatch [:chat.ui/set-input-content-height new-height]))))
|
||||
|
||||
(defn scroll
|
||||
[event
|
||||
{:keys [gradient-z-index focused?]}
|
||||
{:keys [gradient-opacity]}
|
||||
{:keys [lines max-lines]}]
|
||||
(let [y (oops/oget event "nativeEvent.contentOffset.y")]
|
||||
(when (utils/show-top-gradient? y lines max-lines gradient-opacity focused?)
|
||||
(reset! gradient-z-index 1)
|
||||
(js/setTimeout #(reanimated/animate gradient-opacity 1) 0))
|
||||
(when (utils/hide-top-gradient? y gradient-opacity)
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(js/setTimeout #(reset! gradient-z-index 0) 300))))
|
||||
|
||||
(defn change-text
|
||||
[text
|
||||
{:keys [input-ref]}
|
||||
{:keys [text-value cursor-position]}]
|
||||
(reset! text-value text)
|
||||
(reagent/next-tick #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start @cursor-position
|
||||
:end @cursor-position}}))))
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text text]))
|
||||
|
||||
(defn selection-change
|
||||
[event {:keys [lock-selection? cursor-position]}]
|
||||
(when-not @lock-selection?
|
||||
(reset! cursor-position (oops/oget event "nativeEvent.selection.end"))))
|
||||
|
||||
(defn layout
|
||||
[event
|
||||
{:keys [lock-layout?]}
|
||||
blur-height]
|
||||
(when (utils/update-blur-height? event lock-layout? blur-height)
|
||||
(reanimated/set-shared-value blur-height (oops/oget event "nativeEvent.layout.height"))))
|
|
@ -0,0 +1,24 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.images.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
|
||||
(def image-container
|
||||
{:padding-top 12
|
||||
:padding-bottom 8
|
||||
:padding-right 12})
|
||||
|
||||
(def remove-photo-container
|
||||
{:width 14
|
||||
:height 14
|
||||
:border-radius 7
|
||||
:background-color colors/neutral-50
|
||||
:position :absolute
|
||||
:top 5
|
||||
:right 5
|
||||
:justify-content :center
|
||||
:align-items :center})
|
||||
|
||||
(def small-image
|
||||
{:width 56
|
||||
:height 56
|
||||
:border-radius 8})
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.images.view
|
||||
(:require [quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.images.style :as style]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
|
||||
(defn image
|
||||
[item]
|
||||
[rn/view style/image-container
|
||||
[rn/image
|
||||
{:source {:uri (:resized-uri (val item))}
|
||||
:style style/small-image}]
|
||||
[rn/touchable-opacity
|
||||
{:on-press #(rf/dispatch [:chat.ui/image-unselected (val item)])
|
||||
:style style/remove-photo-container
|
||||
:hit-slop {:right 5
|
||||
:left 5
|
||||
:top 10
|
||||
:bottom 10}}
|
||||
[quo/icon :i/close {:color colors/white :size 12}]]])
|
||||
|
||||
(defn images-list
|
||||
[]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [images (rf/sub [:chats/sending-image])
|
||||
height (reanimated/use-shared-value (if (seq images) constants/images-container-height 0))]
|
||||
(rn/use-effect (fn []
|
||||
(reanimated/animate height
|
||||
(if (seq images) constants/images-container-height 0)))
|
||||
[images])
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
|
||||
[rn/flat-list
|
||||
{:key-fn first
|
||||
:render-fn image
|
||||
:data images
|
||||
:horizontal true
|
||||
:keyboard-should-persist-taps :handled}]]))])
|
|
@ -0,0 +1,74 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.keyboard
|
||||
(:require [oops.core :as oops]
|
||||
[status-im.async-storage.core :as async-storage]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
|
||||
(defn get-kb-height
|
||||
[curr-height default-height]
|
||||
(if (and default-height (< curr-height default-height))
|
||||
default-height
|
||||
curr-height))
|
||||
|
||||
(defn store-kb-height
|
||||
[{:keys [kb-default-height]} keyboard-height]
|
||||
(when (and (not @kb-default-height) (pos? keyboard-height))
|
||||
(async-storage/set-item! :kb-default-height (str keyboard-height))))
|
||||
|
||||
(defn handle-emoji-kb-ios
|
||||
[event
|
||||
{:keys [emoji-kb-extra-height]}
|
||||
{:keys [text-value]}
|
||||
{:keys [height saved-height]}
|
||||
{:keys [max-height]}]
|
||||
(let [start-h (oops/oget event "startCoordinates.height")
|
||||
end-h (oops/oget event "endCoordinates.height")
|
||||
diff (- end-h start-h)
|
||||
max (- max-height diff)
|
||||
curr-text @text-value]
|
||||
(if (> (reanimated/get-shared-value height) max)
|
||||
(do
|
||||
(reanimated/set-shared-value height (- (reanimated/get-shared-value height) diff))
|
||||
(reanimated/set-shared-value saved-height (- (reanimated/get-shared-value saved-height) diff))
|
||||
(reset! text-value (str @text-value " "))
|
||||
(js/setTimeout #(reset! text-value curr-text) 0)
|
||||
(reset! emoji-kb-extra-height diff))
|
||||
(when @emoji-kb-extra-height
|
||||
(reanimated/set-shared-value height
|
||||
(+ (reanimated/get-shared-value height) @emoji-kb-extra-height))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value saved-height)
|
||||
@emoji-kb-extra-height))
|
||||
(reset! emoji-kb-extra-height nil)))))
|
||||
|
||||
(defn add-kb-listeners
|
||||
[{:keys [keyboard-show-listener keyboard-frame-listener keyboard-hide-listener input-ref] :as props}
|
||||
state animations dimensions keyboard-height]
|
||||
(reset! keyboard-show-listener (.addListener rn/keyboard
|
||||
"keyboardDidShow"
|
||||
#(store-kb-height state keyboard-height)))
|
||||
(reset! keyboard-frame-listener (.addListener
|
||||
rn/keyboard
|
||||
"keyboardWillChangeFrame"
|
||||
#(handle-emoji-kb-ios % props state animations dimensions)))
|
||||
(reset! keyboard-hide-listener (.addListener rn/keyboard
|
||||
"keyboardDidHide"
|
||||
#(when (and platform/android? @input-ref)
|
||||
(.blur ^js @input-ref)))))
|
||||
|
||||
(defn handle-refocus-emoji-kb-ios
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
{:keys [height saved-height last-height]}
|
||||
{:keys [lines max-lines]}]
|
||||
(when @saved-emoji-kb-extra-height
|
||||
(js/setTimeout (fn []
|
||||
(when (> lines max-lines)
|
||||
(reanimated/animate height
|
||||
(+ (reanimated/get-shared-value last-height)
|
||||
@saved-emoji-kb-extra-height))
|
||||
(reanimated/set-shared-value saved-height
|
||||
(+ (reanimated/get-shared-value last-height)
|
||||
@saved-emoji-kb-extra-height)))
|
||||
(reset! saved-emoji-kb-extra-height nil))
|
||||
600)))
|
|
@ -0,0 +1,101 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[quo2.foundations.typography :as typography]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
|
||||
(defn shadow
|
||||
[lines]
|
||||
(if platform/ios?
|
||||
{:shadow-radius 20
|
||||
:shadow-opacity (colors/theme-colors 0.1 0.7)
|
||||
:shadow-color colors/neutral-100
|
||||
:shadow-offset {:width 0 :height (colors/theme-colors -4 -8)}}
|
||||
{:elevation (if (> lines 1) 10 0)}))
|
||||
|
||||
(defn sheet-container
|
||||
[insets opacity lines]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity}
|
||||
(merge
|
||||
{:border-top-left-radius 20
|
||||
:border-top-right-radius 20
|
||||
:padding-horizontal 20
|
||||
:position :absolute
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-95)
|
||||
:z-index 3
|
||||
:padding-bottom (:bottom insets)}
|
||||
(shadow lines))))
|
||||
|
||||
(def bar-container
|
||||
{:height constants/bar-container-height
|
||||
:left 0
|
||||
:right 0
|
||||
:top 0
|
||||
:z-index 1
|
||||
:justify-content :center
|
||||
:align-items :center})
|
||||
|
||||
(defn bar
|
||||
[]
|
||||
{:width 32
|
||||
:height 4
|
||||
:border-radius 100
|
||||
:background-color (colors/theme-colors colors/neutral-100-opa-5 colors/white-opa-10)})
|
||||
|
||||
(defn input-container
|
||||
[height max-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:height height}
|
||||
{:max-height max-height
|
||||
:overflow :hidden}))
|
||||
|
||||
(defn input
|
||||
[maximized? saved-keyboard-height]
|
||||
(merge typography/paragraph-1
|
||||
{:min-height constants/input-height
|
||||
:color (colors/theme-colors :black :white)
|
||||
:text-align-vertical :top
|
||||
:flex 1
|
||||
:z-index 1
|
||||
:position (if saved-keyboard-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or maximized? platform/ios?) 0)}))
|
||||
|
||||
(defn background
|
||||
[opacity background-y window-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:opacity opacity
|
||||
:transform [{:translate-y background-y}]}
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:height window-height
|
||||
:background-color colors/neutral-95-opa-70
|
||||
:z-index 1}))
|
||||
|
||||
(defn blur-container
|
||||
[height]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:height height}
|
||||
{:position :absolute
|
||||
:elevation 10
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:border-top-right-radius 20
|
||||
:border-top-left-radius 20
|
||||
:overflow :hidden}))
|
||||
|
||||
(defn blur-view
|
||||
[]
|
||||
{:style {:flex 1}
|
||||
:blur-radius (if platform/ios? 20 10)
|
||||
:blur-type (colors/theme-colors :light :dark)
|
||||
:blur-amount 20})
|
|
@ -0,0 +1,18 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.sub-view
|
||||
(:require
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.style :as style]))
|
||||
|
||||
(defn bar
|
||||
[]
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style (style/bar)}]])
|
||||
|
||||
(defn blur-view
|
||||
[layout-height]
|
||||
[:f>
|
||||
(fn []
|
||||
[reanimated/view {:style (style/blur-container layout-height)}
|
||||
[blur/view (style/blur-view)]])])
|
|
@ -0,0 +1,76 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.utils
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
|
||||
(defn bounded-val
|
||||
[val min-val max-val]
|
||||
(max min-val (min val max-val)))
|
||||
|
||||
(defn get-min-height
|
||||
[lines]
|
||||
(if (> lines 1) constants/multiline-minimized-height constants/input-height))
|
||||
|
||||
(defn calc-reopen-height
|
||||
[text-value min-height content-height saved-height]
|
||||
(if (empty? @text-value)
|
||||
min-height
|
||||
(Math/min @content-height (reanimated/get-shared-value saved-height))))
|
||||
|
||||
(defn update-height?
|
||||
[content-size height max-height maximized?]
|
||||
(when-not @maximized?
|
||||
(let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))]
|
||||
(and (not= (reanimated/get-shared-value height) max-height)
|
||||
(> diff constants/content-change-threshold)))))
|
||||
|
||||
(defn show-top-gradient?
|
||||
[y lines max-lines gradient-opacity focused?]
|
||||
(and
|
||||
(> y constants/line-height)
|
||||
(>= lines max-lines)
|
||||
(= (reanimated/get-shared-value gradient-opacity) 0)
|
||||
@focused?))
|
||||
|
||||
(defn hide-top-gradient?
|
||||
[y gradient-opacity]
|
||||
(and
|
||||
(<= y constants/line-height)
|
||||
(= (reanimated/get-shared-value gradient-opacity) 1)))
|
||||
|
||||
(defn show-bottom-gradient?
|
||||
[{:keys [text-value focused?]} {:keys [lines]}]
|
||||
(and (not-empty @text-value) (not @focused?) (> lines 2)))
|
||||
|
||||
(defn show-background?
|
||||
[saved-height max-height new-height]
|
||||
(or (= (reanimated/get-shared-value saved-height) max-height)
|
||||
(> new-height (* constants/background-threshold max-height))))
|
||||
|
||||
(defn update-blur-height?
|
||||
[event lock-layout? layout-height]
|
||||
(or (not @lock-layout?)
|
||||
(> (reanimated/get-shared-value layout-height) (oops/oget event "nativeEvent.layout.height"))))
|
||||
|
||||
(defn calc-lines
|
||||
[height]
|
||||
(let [lines (Math/round (/ height constants/line-height))]
|
||||
(if platform/ios? lines (dec lines))))
|
||||
|
||||
(defn calc-max-height
|
||||
[window-height kb-height insets images]
|
||||
(let [margin-top (if platform/ios? (:top insets) (+ 10 (:top insets)))
|
||||
max-height (- window-height
|
||||
margin-top
|
||||
kb-height
|
||||
constants/bar-container-height
|
||||
constants/actions-container-height)]
|
||||
(if (seq images)
|
||||
(- max-height constants/images-container-height)
|
||||
max-height)))
|
||||
|
||||
(defn empty-input?
|
||||
[input-text images]
|
||||
(and (nil? input-text) (empty? images)))
|
|
@ -0,0 +1,142 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.view
|
||||
(:require
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.hooks :as hooks]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as reagent]
|
||||
[utils.i18n :as i18n]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.style :as style]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.images.view :as images]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.actions.view :as actions]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.sub-view :as sub-view]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.effects :as effects]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.gesture :as drag-gesture]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.handlers :as handler]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.gradients.view :as gradients]))
|
||||
|
||||
(defn sheet
|
||||
[insets window-height blur-height opacity background-y]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [props {:input-ref (atom nil)
|
||||
:keyboard-show-listener (atom nil)
|
||||
:keyboard-frame-listener (atom nil)
|
||||
:keyboard-hide-listener (atom nil)
|
||||
:emoji-kb-extra-height (atom nil)
|
||||
:saved-emoji-kb-extra-height (atom nil)}
|
||||
state {:text-value (reagent/atom "")
|
||||
:cursor-position (reagent/atom 0)
|
||||
:saved-cursor-position (reagent/atom 0)
|
||||
:gradient-z-index (reagent/atom 0)
|
||||
:kb-default-height (reagent/atom nil)
|
||||
:gesture-enabled? (reagent/atom true)
|
||||
:lock-selection? (reagent/atom true)
|
||||
:focused? (reagent/atom false)
|
||||
:lock-layout? (reagent/atom false)
|
||||
:maximized? (reagent/atom false)}]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [images (rf/sub [:chats/sending-image])
|
||||
{:keys [input-text input-content-height]
|
||||
:as chat-input} (rf/sub [:chats/current-chat-input])
|
||||
content-height (reagent/atom (or input-content-height
|
||||
constants/input-height))
|
||||
{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard)
|
||||
kb-height (kb/get-kb-height keyboard-height
|
||||
@(:kb-default-height state))
|
||||
max-height (utils/calc-max-height window-height
|
||||
kb-height
|
||||
insets
|
||||
images)
|
||||
lines (utils/calc-lines @content-height)
|
||||
max-lines (utils/calc-lines max-height)
|
||||
initial-height (if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height)
|
||||
animations {:gradient-opacity (reanimated/use-shared-value
|
||||
0)
|
||||
:container-opacity (reanimated/use-shared-value
|
||||
(if (utils/empty-input?
|
||||
input-text
|
||||
images)
|
||||
0.7
|
||||
1))
|
||||
:height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:saved-height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:last-height (reanimated/use-shared-value
|
||||
(utils/bounded-val
|
||||
@content-height
|
||||
constants/input-height
|
||||
max-height))
|
||||
:opacity opacity
|
||||
:background-y background-y}
|
||||
dimensions {:content-height content-height
|
||||
:max-height max-height
|
||||
:window-height window-height
|
||||
:lines lines
|
||||
:max-lines max-lines}
|
||||
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)]
|
||||
(effects/initialize props
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
chat-input
|
||||
keyboard-height
|
||||
(seq images))
|
||||
[gesture/gesture-detector
|
||||
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
|
||||
[reanimated/view
|
||||
{:style (style/sheet-container insets (:container-opacity animations) lines)
|
||||
:on-layout #(handler/layout % state blur-height)}
|
||||
[sub-view/bar]
|
||||
[reanimated/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
|
||||
:style (style/input-container (:height animations) max-height)
|
||||
:accessibility-label :message-input-container}
|
||||
[rn/text-input
|
||||
{:ref #(reset! (:input-ref props) %)
|
||||
:default-value @(:text-value state)
|
||||
:on-focus #(handler/focus props state animations dimensions)
|
||||
:on-blur #(handler/blur state animations dimensions images)
|
||||
:on-content-size-change #(handler/content-size-change %
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
keyboard-shown)
|
||||
:on-scroll #(handler/scroll % state animations dimensions)
|
||||
:on-change-text #(handler/change-text % props state)
|
||||
:on-selection-change #(handler/selection-change % state)
|
||||
:max-height max-height
|
||||
:max-font-size-multiplier 1
|
||||
:multiline true
|
||||
:placeholder (i18n/label :t/type-something)
|
||||
:placeholder-text-color (colors/theme-colors colors/neutral-40 colors/neutral-50)
|
||||
:style (style/input @(:maximized? state)
|
||||
@(:saved-emoji-kb-extra-height props))
|
||||
:accessibility-label :chat-message-input}]
|
||||
[gradients/view props state animations show-bottom-gradient?]]
|
||||
[images/images-list]
|
||||
[actions/view props state animations window-height insets (seq images)]]]))]))])
|
||||
|
||||
(defn bottom-sheet-composer
|
||||
[insets]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [window-height (rf/sub [:dimensions/window-height])
|
||||
opacity (reanimated/use-shared-value 0)
|
||||
background-y (reanimated/use-shared-value (- window-height))
|
||||
blur-height (reanimated/use-shared-value (+ constants/composer-default-height
|
||||
(:bottom insets)))]
|
||||
[rn/view
|
||||
[reanimated/view {:style (style/background opacity background-y window-height)}]
|
||||
[sub-view/blur-view blur-height]
|
||||
[sheet insets window-height blur-height opacity background-y]]))])
|
|
@ -17,7 +17,7 @@
|
|||
;; TODO (flexsurfer) probably we don't want reactions here
|
||||
(if (or deleted? deleted-for-me?)
|
||||
[content.deleted/deleted-message message context]
|
||||
[message/message-with-reactions message context]))
|
||||
[message/message-with-reactions message context false]))
|
||||
|
||||
(defn pinned-messages
|
||||
[chat-id]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
(ns status-im2.contexts.chat.messages.content.view
|
||||
(:require [react-native.core :as rn]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.platform :as platform]
|
||||
[status-im2.contexts.chat.messages.content.style :as style]
|
||||
[status-im2.contexts.chat.messages.content.pin.view :as pin]
|
||||
[status-im2.constants :as constants]
|
||||
|
@ -86,7 +87,8 @@
|
|||
[]
|
||||
(let [show-delivery-state? (reagent/atom false)]
|
||||
(fn [{:keys [content-type quoted-message content outgoing outgoing-status] :as message-data}
|
||||
{:keys [chat-id] :as context}]
|
||||
{:keys [chat-id] :as context}
|
||||
keyboard-shown]
|
||||
(let [first-image (first (:album message-data))
|
||||
outgoing-status (if (= content-type constants/content-type-album)
|
||||
(:outgoing-status first-image)
|
||||
|
@ -104,12 +106,14 @@
|
|||
:style {:border-radius 16
|
||||
:opacity (if (and outgoing (= outgoing-status :sending)) 0.5 1)}
|
||||
:on-press (fn []
|
||||
(when (and outgoing
|
||||
(not= outgoing-status :sending)
|
||||
(not @show-delivery-state?))
|
||||
(reset! show-delivery-state? true)
|
||||
(js/setTimeout #(reset! show-delivery-state? false)
|
||||
delivery-state-showing-time-ms)))
|
||||
(if (and platform/ios? @keyboard-shown)
|
||||
(rn/dismiss-keyboard!)
|
||||
(when (and outgoing
|
||||
(not= outgoing-status :sending)
|
||||
(not @show-delivery-state?))
|
||||
(reset! show-delivery-state? true)
|
||||
(js/setTimeout #(reset! show-delivery-state? false)
|
||||
delivery-state-showing-time-ms))))
|
||||
:on-long-press #(on-long-press message-data context)}
|
||||
[rn/view {:style {:padding-vertical 8}}
|
||||
(when (and (seq response-to) quoted-message)
|
||||
|
@ -147,7 +151,8 @@
|
|||
|
||||
(defn message-with-reactions
|
||||
[{:keys [pinned-by mentioned in-pinned-view? content-type last-in-group? message-id] :as message-data}
|
||||
{:keys [chat-id] :as context}]
|
||||
{:keys [chat-id] :as context}
|
||||
keyboard-shown]
|
||||
[rn/view
|
||||
{:style (style/message-container in-pinned-view? pinned-by mentioned last-in-group?)
|
||||
:accessibility-label :chat-item}
|
||||
|
@ -157,5 +162,5 @@
|
|||
constants/content-type-contact-request}
|
||||
content-type)
|
||||
[system-message-content message-data]
|
||||
[user-message-content message-data context])
|
||||
[user-message-content message-data context keyboard-shown])
|
||||
[reactions/message-reactions-row chat-id message-id]])
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
(ns status-im2.contexts.chat.messages.list.new-temp-view
|
||||
(:require [oops.core :as oops]
|
||||
[quo2.core :as quo]
|
||||
[react-native.background-timer :as background-timer]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.ui.screens.chat.group :as chat.group]
|
||||
[status-im.ui.screens.chat.message.gap :as message.gap]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.chat.messages.content.deleted.view :as content.deleted]
|
||||
[status-im2.contexts.chat.messages.content.view :as message]
|
||||
[status-im2.contexts.chat.messages.list.state :as state]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as composer.constants]))
|
||||
|
||||
(defonce messages-list-ref (atom nil))
|
||||
|
||||
(defn list-key-fn [{:keys [message-id value]}] (or message-id value))
|
||||
(defn list-ref [ref] (reset! messages-list-ref ref))
|
||||
|
||||
(defn scroll-to-bottom
|
||||
[]
|
||||
(some-> ^js @messages-list-ref
|
||||
(.scrollToOffset #js {:y 0 :animated true})))
|
||||
|
||||
(defonce ^:const threshold-percentage-to-show-floating-scroll-down-button 75)
|
||||
(defonce show-floating-scroll-down-button (reagent/atom false))
|
||||
|
||||
(defn on-scroll
|
||||
[evt]
|
||||
(let [y (oops/oget evt "nativeEvent.contentOffset.y")
|
||||
layout-height (oops/oget evt "nativeEvent.layoutMeasurement.height")
|
||||
threshold-height (* (/ layout-height 100)
|
||||
threshold-percentage-to-show-floating-scroll-down-button)
|
||||
reached-threshold? (> y threshold-height)]
|
||||
(when (not= reached-threshold? @show-floating-scroll-down-button)
|
||||
(rn/configure-next (:ease-in-ease-out rn/layout-animation-presets))
|
||||
(reset! show-floating-scroll-down-button reached-threshold?))))
|
||||
|
||||
(defn on-viewable-items-changed
|
||||
[evt]
|
||||
(when @messages-list-ref
|
||||
(reset! state/first-not-visible-item
|
||||
(when-let [last-visible-element (aget (oops/oget evt "viewableItems")
|
||||
(dec (oops/oget evt "viewableItems.length")))]
|
||||
(let [index (oops/oget last-visible-element "index")
|
||||
;; Get first not visible element, if it's a datemark/gap
|
||||
;; we might unnecessarely add messages on receiving as
|
||||
;; they do not have a clock value, but most of the times
|
||||
;; it will be a message
|
||||
first-not-visible (aget (oops/oget @messages-list-ref "props.data") (inc index))]
|
||||
(when (and first-not-visible
|
||||
(= :message (:type first-not-visible)))
|
||||
first-not-visible))))))
|
||||
|
||||
;;TODO this is not really working in pair with inserting new messages because we stop inserting new
|
||||
;;messages
|
||||
;;if they outside the viewarea, but we load more here because end is reached,so its slowdown UI because
|
||||
;;we
|
||||
;;load and render 20 messages more, but we can't prevent this , because otherwise :on-end-reached will
|
||||
;;work wrong
|
||||
(defn list-on-end-reached
|
||||
[]
|
||||
(if @state/scrolling
|
||||
(rf/dispatch [:chat.ui/load-more-messages-for-current-chat])
|
||||
(background-timer/set-timeout #(rf/dispatch [:chat.ui/load-more-messages-for-current-chat])
|
||||
(if platform/low-device? 700 200))))
|
||||
|
||||
(defonce messages-view-height (reagent/atom 0))
|
||||
|
||||
(defn on-messages-view-layout
|
||||
[evt]
|
||||
(reset! messages-view-height (oops/oget evt "nativeEvent.layout.height")))
|
||||
|
||||
(defn list-footer
|
||||
[{:keys [chat-id]}]
|
||||
(let [loading-messages? (rf/sub [:chats/loading-messages? chat-id])
|
||||
all-loaded? (rf/sub [:chats/all-loaded? chat-id])]
|
||||
(when (or loading-messages? (not chat-id) (not all-loaded?))
|
||||
[rn/view {:style (when platform/android? {:scaleY -1})}
|
||||
[quo/skeleton @messages-view-height]])))
|
||||
|
||||
(defn list-header
|
||||
[{:keys [chat-id chat-type invitation-admin]}]
|
||||
(when (= chat-type constants/private-group-chat-type)
|
||||
[rn/view {:style (when platform/android? {:scaleY -1})}
|
||||
[chat.group/group-chat-footer chat-id invitation-admin]]))
|
||||
|
||||
(defn render-fn
|
||||
[{:keys [type value deleted? deleted-for-me? content-type] :as message-data} _ _
|
||||
{:keys [context keyboard-shown]}]
|
||||
[rn/view {:style (when platform/android? {:scaleY -1})}
|
||||
(if (= type :datemark)
|
||||
[quo/divider-date value]
|
||||
(if (= content-type constants/content-type-gap)
|
||||
[not-implemented/not-implemented
|
||||
[message.gap/gap message-data]]
|
||||
[rn/view {:padding-horizontal 8}
|
||||
(if (or deleted? deleted-for-me?)
|
||||
[content.deleted/deleted-message message-data context]
|
||||
[message/message-with-reactions message-data context keyboard-shown])]))])
|
||||
|
||||
(defn messages-list-content
|
||||
[{:keys [chat-id] :as chat} insets keyboard-shown]
|
||||
(fn []
|
||||
(let [context (rf/sub [:chats/current-chat-message-list-view-context])
|
||||
messages (rf/sub [:chats/raw-chat-messages-stream chat-id])]
|
||||
[rn/view
|
||||
{:style {:flex 1}}
|
||||
;; NOTE: DO NOT use anonymous functions for handlers
|
||||
[rn/flat-list
|
||||
{:key-fn list-key-fn
|
||||
:ref list-ref
|
||||
:header [list-header chat]
|
||||
:footer [list-footer chat]
|
||||
:data messages
|
||||
:render-data {:context context
|
||||
:keyboard-shown keyboard-shown}
|
||||
:render-fn render-fn
|
||||
:on-viewable-items-changed on-viewable-items-changed
|
||||
:on-end-reached list-on-end-reached
|
||||
:on-scroll-to-index-failed identity ; don't remove this
|
||||
:content-container-style {:padding-top (+ composer.constants/composer-default-height
|
||||
(:bottom insets)
|
||||
32)
|
||||
:padding-bottom 16}
|
||||
:scroll-indicator-insets {:top (+ composer.constants/composer-default-height
|
||||
(:bottom insets))}
|
||||
:keyboard-dismiss-mode :interactive
|
||||
:keyboard-should-persist-taps :handled
|
||||
:on-momentum-scroll-begin state/start-scrolling
|
||||
:on-momentum-scroll-end state/stop-scrolling
|
||||
:scroll-event-throttle 16
|
||||
:on-scroll on-scroll
|
||||
;; TODO https://github.com/facebook/react-native/issues/30034
|
||||
:inverted (when platform/ios? true)
|
||||
:style (when platform/android? {:scaleY -1})
|
||||
:on-layout on-messages-view-layout}]
|
||||
[quo/floating-shell-button
|
||||
(merge {:jump-to
|
||||
{:on-press #(do
|
||||
(rf/dispatch [:chat/close true])
|
||||
(rf/dispatch [:shell/navigate-to-jump-to]))
|
||||
:label (i18n/label :t/jump-to)}}
|
||||
(when @show-floating-scroll-down-button
|
||||
{:scroll-to-bottom {:on-press scroll-to-bottom}}))
|
||||
{:position :absolute
|
||||
:bottom (+ (:bottom insets) composer.constants/composer-default-height 6)}]])))
|
||||
|
||||
(defn messages-list
|
||||
[chat insets]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [keyboard-show-listener (atom nil)
|
||||
keyboard-hide-listener (atom nil)
|
||||
keyboard-shown (atom false)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(reset! keyboard-show-listener (.addListener rn/keyboard
|
||||
"keyboardWillShow"
|
||||
#(reset! keyboard-shown true)))
|
||||
(reset! keyboard-hide-listener (.addListener rn/keyboard
|
||||
"keyboardWillHide"
|
||||
#(reset! keyboard-shown false)))
|
||||
(fn []
|
||||
(.remove ^js @keyboard-show-listener)
|
||||
(.remove ^js @keyboard-hide-listener))))
|
||||
[messages-list-content chat insets keyboard-shown]))])
|
|
@ -89,7 +89,8 @@
|
|||
[chat.group/group-chat-footer chat-id invitation-admin]]))
|
||||
|
||||
(defn render-fn
|
||||
[{:keys [type value deleted? deleted-for-me? content-type] :as message-data} _ _ context]
|
||||
[{:keys [type value deleted? deleted-for-me? content-type] :as message-data} _ _
|
||||
{:keys [keyboard-shown context]}]
|
||||
[rn/view {:style (when platform/android? {:scaleY -1})}
|
||||
(if (= type :datemark)
|
||||
[quo/divider-date value]
|
||||
|
@ -99,13 +100,14 @@
|
|||
[rn/view {:padding-horizontal 8}
|
||||
(if (or deleted? deleted-for-me?)
|
||||
[content.deleted/deleted-message message-data context]
|
||||
[message/message-with-reactions message-data context])]))])
|
||||
[message/message-with-reactions message-data context keyboard-shown])]))])
|
||||
|
||||
(defn messages-list
|
||||
[{:keys [chat-id] :as chat}]
|
||||
(let [render-data (rf/sub [:chats/current-chat-message-list-view-context])
|
||||
messages (rf/sub [:chats/raw-chat-messages-stream chat-id])
|
||||
bottom-space 15]
|
||||
(let [context (rf/sub [:chats/current-chat-message-list-view-context])
|
||||
messages (rf/sub [:chats/raw-chat-messages-stream chat-id])
|
||||
keyboard-shown (atom false)
|
||||
bottom-space 15]
|
||||
[rn/view
|
||||
{:style {:flex 1}}
|
||||
;; NOTE: DO NOT use anonymous functions for handlers
|
||||
|
@ -115,7 +117,8 @@
|
|||
:header [list-header chat]
|
||||
:footer [list-footer chat]
|
||||
:data messages
|
||||
:render-data render-data
|
||||
:render-data {:context context
|
||||
:keyboard-shown keyboard-shown}
|
||||
:render-fn render-fn
|
||||
:on-viewable-items-changed on-viewable-items-changed
|
||||
:on-end-reached list-on-end-reached
|
||||
|
|
|
@ -73,10 +73,10 @@
|
|||
(fn [insets]
|
||||
[rn/keyboard-avoiding-view
|
||||
{:style {:position :relative :flex 1}
|
||||
:keyboardVerticalOffset (- (max 20 (:bottom insets)))}
|
||||
:keyboardVerticalOffset (- (:bottom insets))}
|
||||
[page-nav]
|
||||
[pin.banner/banner chat-id]
|
||||
[messages.list/messages-list chat]
|
||||
[messages.list/messages-list chat insets]
|
||||
(if-not able-to-send-message?
|
||||
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]
|
||||
[composer/composer chat-id insets])])]))
|
||||
|
|
|
@ -24,13 +24,13 @@
|
|||
(rf/dispatch [:navigate-back]))
|
||||
|
||||
(defn bottom-gradient
|
||||
[selected-images bottom-inset selected]
|
||||
[selected-images insets selected]
|
||||
(when (or (seq @selected) (seq selected-images))
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [:black :transparent]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/gradient-container bottom-inset)}
|
||||
:style (style/gradient-container (:bottom insets))}
|
||||
[quo/button
|
||||
{:style {:align-self :stretch
|
||||
:margin-horizontal 20
|
||||
|
@ -109,11 +109,11 @@
|
|||
(defn photo-selector
|
||||
[{:keys [scroll-enabled on-scroll]}]
|
||||
[:f>
|
||||
(let [{:keys [bottom-inset]} (rf/sub [:screen-params]) ; TODO:
|
||||
; https://github.com/status-im/status-mobile/issues/15535
|
||||
temporary-selected (reagent/atom [])] ; used when switching albums
|
||||
(let [{:keys [insets]} (rf/sub [:get-screen-params]) ; TODO:
|
||||
; https://github.com/status-im/status-mobile/issues/15535
|
||||
temporary-selected (reagent/atom [])] ; used when switching albums
|
||||
(fn []
|
||||
(let [selected (reagent/atom []) ; currently selected
|
||||
(let [selected (reagent/atom []) ; currently selected
|
||||
selected-images (rf/sub [:chats/sending-image]) ; already selected and dispatched
|
||||
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
|
||||
(rn/use-effect
|
||||
|
@ -142,11 +142,11 @@
|
|||
:data camera-roll-photos
|
||||
:num-columns 3
|
||||
:content-container-style {:width "100%"
|
||||
:padding-bottom (+ (:bottom bottom-inset) 100)
|
||||
:padding-bottom (+ (:bottom insets) 100)
|
||||
:padding-top 64}
|
||||
:on-scroll on-scroll
|
||||
:scroll-enabled scroll-enabled
|
||||
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
|
||||
selected-album loading?
|
||||
has-next-page?])}]
|
||||
[bottom-gradient selected-images bottom-inset selected]]))])))])
|
||||
[bottom-gradient selected-images insets selected]]))])))])
|
||||
|
|