parent
c275205bed
commit
341497e98d
|
@ -130,6 +130,9 @@
|
|||
(when (exists? (.-NativeModules ^js react-native))
|
||||
(.-RNSelectableTextInputManager ^js (.-NativeModules ^js react-native))))
|
||||
|
||||
;; TODO: iOS native implementation https://github.com/status-im/status-mobile/issues/14137
|
||||
(defonce selectable-text-input
|
||||
(reagent/adapt-react-class
|
||||
(.requireNativeComponent ^js react-native "RNSelectableTextInput")))
|
||||
(if platform/android?
|
||||
(reagent/adapt-react-class
|
||||
(.requireNativeComponent ^js react-native "RNSelectableTextInput"))
|
||||
view))
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
[status-im.chat.models.mentions :as mentions]
|
||||
[status-im.chat.models.message :as chat.message]
|
||||
[status-im.chat.models.message-content :as message-content]
|
||||
[status-im2.config :as config]
|
||||
[status-im2.constants :as constants]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.i18n :as i18n]
|
||||
|
@ -35,9 +34,12 @@
|
|||
"Set input text for current-chat. Takes db and input text and cofx
|
||||
as arguments and returns new fx. Always clear all validation messages."
|
||||
{:events [:chat.ui/set-chat-input-text]}
|
||||
[{db :db} new-input chat-id]
|
||||
[{:keys [db] :as cofx} new-input chat-id]
|
||||
(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/merge cofx
|
||||
{:db (assoc-in db [:chat/inputs current-chat-id :input-text] (text->emoji new-input))}
|
||||
(when (empty? new-input)
|
||||
(mentions/clear-mentions)))))
|
||||
|
||||
(rf/defn set-input-content-height
|
||||
{:events [:chat.ui/set-input-content-height]}
|
||||
|
@ -111,8 +113,7 @@
|
|||
(update-in [:chat/inputs current-chat-id :metadata]
|
||||
dissoc
|
||||
:sending-image))
|
||||
:dispatch (when-not config/new-composer-enabled?
|
||||
[:mention/to-input-field text current-chat-id])}))
|
||||
:dispatch [:mention/to-input-field text current-chat-id]}))
|
||||
|
||||
(rf/defn show-contact-request-input
|
||||
"Sets reference to previous chat message and focuses on input"
|
||||
|
@ -184,8 +185,6 @@
|
|||
[{:keys [db] :as cofx}]
|
||||
(let [current-chat-id (:current-chat-id db)]
|
||||
(rf/merge cofx
|
||||
(when-not config/new-composer-enabled?
|
||||
{:set-text-input-value [current-chat-id ""]})
|
||||
(clean-input current-chat-id)
|
||||
(mentions/clear-mentions))))
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
[quo.react :as react]
|
||||
[quo.react-native :as rn]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im2.config :as config]
|
||||
[utils.re-frame :as rf]
|
||||
[taoensso.timbre :as log]
|
||||
[native-module.core :as native-module]))
|
||||
|
@ -99,10 +98,9 @@
|
|||
[{:keys [db]} result]
|
||||
(log/debug "[mentions] on-to-input-field-success" {:result result})
|
||||
(let [{:keys [input-segments state chat-id new-text]} (transfer-mention-result result)]
|
||||
{:set-text-input-value [chat-id new-text]
|
||||
:db (-> db
|
||||
(assoc-in [:chats/mentions chat-id :mentions] state)
|
||||
(assoc-in [:chat/inputs-with-mentions chat-id] input-segments))}))
|
||||
{:db (-> db
|
||||
(assoc-in [:chats/mentions chat-id :mentions] state)
|
||||
(assoc-in [:chat/inputs-with-mentions chat-id] input-segments))}))
|
||||
|
||||
(rf/defn on-change-text
|
||||
{:events [:mention/on-change-text]}
|
||||
|
@ -163,25 +161,11 @@
|
|||
:match match
|
||||
:searched-text searched-text
|
||||
:public-key public-key})
|
||||
(let [{:keys [new-text state chat-id]} (transfer-mention-result result)
|
||||
{:keys [at-sign-idx]} state
|
||||
cursor (+ at-sign-idx (count primary-name) 2)]
|
||||
(let [{:keys [new-text chat-id]} (transfer-mention-result result)]
|
||||
(rf/merge
|
||||
cofx
|
||||
(let [common {:db (-> db
|
||||
(assoc-in [:chats/mention-suggestions chat-id] nil))
|
||||
:dispatch [:chat.ui/set-chat-input-text new-text chat-id]}
|
||||
extra (if (not config/new-composer-enabled?)
|
||||
;; NOTE(rasom): Some keyboards do not react on selection property passed to
|
||||
;; text input (specifically Samsung keyboard with predictive text set on).
|
||||
;; In this case, if the user continues typing after the programmatic change,
|
||||
;; the new text is added to the last known cursor position before
|
||||
;; programmatic change. By calling `reset-text-input-cursor` we force the
|
||||
;; keyboard's cursor position to be changed before the next input.
|
||||
{:set-text-input-value [chat-id new-text text-input-ref]
|
||||
:reset-text-input-cursor (reset-text-input-cursor text-input-ref cursor)}
|
||||
{})]
|
||||
(merge common extra))
|
||||
{:db (assoc-in db [:chats/mention-suggestions chat-id] nil)
|
||||
:dispatch [:chat.ui/set-chat-input-text new-text chat-id]}
|
||||
(recheck-at-idxs public-key))))
|
||||
|
||||
(rf/defn clear-suggestions
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.components.edit.style)
|
||||
|
||||
(def container
|
||||
{:flex-direction :row
|
||||
:height 24})
|
||||
|
||||
(def content-container
|
||||
{:padding-horizontal 10
|
||||
:flex 1
|
||||
:flex-direction :row})
|
||||
|
||||
(def icon-container
|
||||
{:position :absolute
|
||||
:left 10
|
||||
:bottom -4
|
||||
:width 16
|
||||
:height 16})
|
||||
|
||||
(def text-container
|
||||
{:position :absolute
|
||||
:left 36
|
||||
:right 54
|
||||
:top 3
|
||||
:flex-direction :row
|
||||
:align-items :center})
|
|
@ -1,33 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.components.edit.view
|
||||
(:require [utils.i18n :as i18n]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[re-frame.core :as rf]
|
||||
[react-native.core :as rn]
|
||||
[status-im.ui2.screens.chat.components.edit.style :as style]))
|
||||
|
||||
(defn edit-message
|
||||
[on-cancel]
|
||||
[rn/view
|
||||
{:style style/container
|
||||
:accessibility-label :edit-message}
|
||||
[rn/view {:style style/content-container}
|
||||
[quo/icon :i/connector
|
||||
{:size 16
|
||||
:color (colors/theme-colors colors/neutral-40 colors/neutral-60)
|
||||
:container-style style/icon-container}]
|
||||
[rn/view {:style style/text-container}
|
||||
[quo/text
|
||||
{:weight :medium
|
||||
:size :paragraph-2}
|
||||
(i18n/label :t/editing-message)]]]
|
||||
[quo/button
|
||||
{:width 24
|
||||
:size 24
|
||||
:accessibility-label :reply-cancel-button
|
||||
:on-press #(do (on-cancel)
|
||||
(rf/dispatch [:chat.ui/cancel-message-edit]))
|
||||
:type :outline}
|
||||
[quo/icon :i/close
|
||||
{:size 16
|
||||
:color (colors/theme-colors colors/neutral-100 colors/neutral-40)}]]])
|
|
@ -1,37 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.components.reply.style)
|
||||
|
||||
(defn reply-content
|
||||
[pin?]
|
||||
{:padding-horizontal (when-not pin? 10)
|
||||
:flex 1
|
||||
:flex-direction :row})
|
||||
|
||||
(defn quoted-message
|
||||
[pin? in-chat-input?]
|
||||
(merge {:flex-direction :row
|
||||
:flex 1
|
||||
:align-items :center
|
||||
:width (if in-chat-input? "100%" "45%")}
|
||||
(when-not pin?
|
||||
{:left 22
|
||||
:margin-right 22})))
|
||||
|
||||
(def reply-from
|
||||
{:flex-direction :row
|
||||
:align-items :center})
|
||||
|
||||
(def message-author-text
|
||||
{:margin-left 4})
|
||||
|
||||
(def message-text
|
||||
{:text-transform :none
|
||||
:margin-left 4
|
||||
:margin-top 2
|
||||
:flex 1})
|
||||
|
||||
(def gradient
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:top 0
|
||||
:bottom 0
|
||||
:width "50%"})
|
|
@ -1,142 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.components.reply.view
|
||||
(:require [clojure.string :as string]
|
||||
[utils.i18n :as i18n]
|
||||
[quo.react-native :as rn]
|
||||
[quo2.components.buttons.button :as quo2.button]
|
||||
[quo2.components.icon :as quo2.icon]
|
||||
[quo2.components.markdown.text :as quo2.text]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im.ethereum.stateofus :as stateofus]
|
||||
[status-im.ui.components.icons.icons :as icons]
|
||||
[status-im2.contexts.chat.messages.avatar.view :as avatar]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im.ui2.screens.chat.components.reply.style :as style]
|
||||
[react-native.linear-gradient :as linear-gradient]))
|
||||
|
||||
(defn get-quoted-text-with-mentions
|
||||
[parsed-text]
|
||||
(string/join
|
||||
(mapv (fn [{:keys [type literal children]}]
|
||||
(cond
|
||||
(= type "paragraph")
|
||||
(get-quoted-text-with-mentions children)
|
||||
|
||||
(= type "mention")
|
||||
(rf/sub [:messages/resolve-mention literal])
|
||||
|
||||
(seq children)
|
||||
(get-quoted-text-with-mentions children)
|
||||
|
||||
:else
|
||||
literal))
|
||||
parsed-text)))
|
||||
|
||||
(defn format-author
|
||||
[contact-name]
|
||||
(let [author (if (or (= (aget contact-name 0) "@")
|
||||
;; in case of replies
|
||||
(= (aget contact-name 1) "@"))
|
||||
(or (stateofus/username contact-name)
|
||||
(subs contact-name 0 81))
|
||||
contact-name)]
|
||||
author))
|
||||
|
||||
(defn format-reply-author
|
||||
[from username current-public-key]
|
||||
(or (and (= from current-public-key)
|
||||
(i18n/label :t/You))
|
||||
(when username (format-author username))))
|
||||
|
||||
(defn reply-deleted-message
|
||||
[]
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
:align-items :center}}
|
||||
[quo2.icon/icon :sad-face {:size 16}]
|
||||
[quo2.text/text
|
||||
{:number-of-lines 1
|
||||
:size :label
|
||||
:weight :regular
|
||||
:accessibility-label :quoted-message
|
||||
:style {:text-transform :none
|
||||
:margin-left 4
|
||||
:margin-top 2}}
|
||||
(i18n/label :t/message-deleted)]])
|
||||
|
||||
(defn reply-from
|
||||
[{:keys [from contact-name current-public-key]}]
|
||||
[rn/view {:style style/reply-from}
|
||||
[avatar/avatar from :xxxs]
|
||||
[quo2.text/text
|
||||
{:weight :semi-bold
|
||||
:size :paragraph-2
|
||||
:number-of-lines 1
|
||||
:style style/message-author-text}
|
||||
(format-reply-author from contact-name current-public-key)]])
|
||||
|
||||
(defn reply-message
|
||||
[{:keys [from identicon content-type contentType parsed-text content deleted? deleted-for-me?
|
||||
album-images-count]}
|
||||
in-chat-input? pin? recording-audio?]
|
||||
(let [contact-name (rf/sub [:contacts/contact-name-by-identity from])
|
||||
current-public-key (rf/sub [:multiaccount/public-key])
|
||||
content-type (or content-type contentType)]
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
:height (when-not pin? 24)
|
||||
:accessibility-label :reply-message}}
|
||||
[rn/view {:style (style/reply-content pin?)}
|
||||
(when-not pin?
|
||||
;;TODO quo2 icon should be used
|
||||
[icons/icon :main-icons/connector
|
||||
{:color (colors/theme-colors colors/neutral-40 colors/neutral-60)
|
||||
:container-style {:position :absolute :left 10 :bottom -4 :width 16 :height 16}}])
|
||||
(if (or deleted? deleted-for-me?)
|
||||
[rn/view {:style (style/quoted-message pin? in-chat-input?)}
|
||||
[reply-deleted-message]]
|
||||
[rn/view {:style (style/quoted-message pin? in-chat-input?)}
|
||||
[reply-from
|
||||
{:from from
|
||||
:identicon identicon
|
||||
:contact-name contact-name
|
||||
:current-public-key current-public-key}]
|
||||
[quo2.text/text
|
||||
{:number-of-lines 1
|
||||
:size :label
|
||||
:weight :regular
|
||||
:accessibility-label :quoted-message
|
||||
:ellipsize-mode :tail
|
||||
:style (merge
|
||||
style/message-text
|
||||
(when (or (= constants/content-type-image content-type)
|
||||
(= constants/content-type-sticker content-type)
|
||||
(= constants/content-type-audio content-type))
|
||||
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}))}
|
||||
(case (or content-type contentType)
|
||||
constants/content-type-image (if album-images-count
|
||||
(i18n/label :t/images-albums-count
|
||||
{:album-images-count album-images-count})
|
||||
(i18n/label :t/image))
|
||||
constants/content-type-sticker (i18n/label :t/sticker)
|
||||
constants/content-type-audio (i18n/label :t/audio)
|
||||
(get-quoted-text-with-mentions (or parsed-text (:parsed-text content))))]])]
|
||||
(when (and in-chat-input? (not recording-audio?))
|
||||
[quo2.button/button
|
||||
{:width 24
|
||||
:size 24
|
||||
:type :outline
|
||||
:accessibility-label :reply-cancel-button
|
||||
:on-press #(rf/dispatch [:chat.ui/cancel-message-reply])}
|
||||
;;TODO quo2 icon should be used
|
||||
[icons/icon :main-icons/close
|
||||
{:width 16
|
||||
:height 16
|
||||
:color (colors/theme-colors colors/neutral-100 colors/neutral-40)}]])
|
||||
(when (and in-chat-input? recording-audio?)
|
||||
[linear-gradient/linear-gradient
|
||||
{:colors [(colors/theme-colors colors/white-opa-0 colors/neutral-90-opa-0)
|
||||
(colors/theme-colors colors/white colors/neutral-90)]
|
||||
:start {:x 0 :y 0}
|
||||
:end {:x 0.7 :y 0}
|
||||
:style style/gradient}])]))
|
|
@ -1,5 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.edit.style)
|
||||
|
||||
(def container
|
||||
{:padding-horizontal 15
|
||||
:padding-vertical 8})
|
|
@ -1,23 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.edit.view
|
||||
(:require [react-native.core :as rn]
|
||||
[status-im.ui2.screens.chat.components.edit.view :as edit]
|
||||
[status-im.ui2.screens.chat.composer.edit.style :as style]
|
||||
[status-im.ui2.screens.chat.composer.input :as input]))
|
||||
|
||||
(defn- focus-input-on-edit
|
||||
[edit had-edit text-input-ref]
|
||||
;;when we show edit we focus input
|
||||
(when-not (= edit @had-edit)
|
||||
(reset! had-edit edit)
|
||||
(when edit
|
||||
;; A setTimeout of 0 is necessary to ensure the statement is enqueued and will get executed ASAP.
|
||||
(js/setTimeout #(input/input-focus text-input-ref) 0))))
|
||||
|
||||
(defn edit-message-auto-focus-wrapper
|
||||
[text-input-ref _]
|
||||
(let [had-edit (atom nil)]
|
||||
(fn [_ edit on-cancel]
|
||||
(focus-input-on-edit edit had-edit text-input-ref)
|
||||
(when edit
|
||||
[rn/view {:style style/container}
|
||||
[edit/edit-message on-cancel]]))))
|
|
@ -1,11 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.images.style)
|
||||
|
||||
(def remove-photo-container
|
||||
{:position :absolute
|
||||
:top 5
|
||||
:right 5})
|
||||
|
||||
(def small-image
|
||||
{:width 56
|
||||
:height 56
|
||||
:border-radius 8})
|
|
@ -1,35 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.images.view
|
||||
(:require [quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[re-frame.core :as rf]
|
||||
[react-native.core :as rn]
|
||||
[status-im.ui2.screens.chat.composer.images.style :as style]))
|
||||
|
||||
(defn image
|
||||
[item]
|
||||
;; there is a bug on Android https://github.com/facebook/react-native/issues/12534
|
||||
;; so we need some magic here with paddings so close button isn't cut
|
||||
[rn/view {:padding-top 12 :padding-bottom 8 :padding-right 12}
|
||||
[rn/image
|
||||
{:source {:uri (:resized-uri (val item))}
|
||||
:style style/small-image}]
|
||||
[rn/touchable-opacity
|
||||
{:on-press (fn [] (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/clear
|
||||
{:size 20
|
||||
:color (colors/theme-colors colors/neutral-50 colors/neutral-60)}]]])
|
||||
|
||||
(defn images-list
|
||||
[images]
|
||||
(when (seq images)
|
||||
[rn/flat-list
|
||||
{:key-fn first
|
||||
:render-fn image
|
||||
:data images
|
||||
:horizontal true
|
||||
:keyboard-should-persist-taps :handled}]))
|
|
@ -1,383 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.input
|
||||
(:require [clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.constants :as chat.constants]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.transforms :as transforms]
|
||||
[quo2.foundations.typography :as typography]
|
||||
[react-native.background-timer :as background-timer]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.core :as rn]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[quo.react :as quo.react]))
|
||||
|
||||
(defonce input-texts (atom {}))
|
||||
(defonce input-text-content-heights (atom {}))
|
||||
(defonce mentions-enabled? (reagent/atom {}))
|
||||
(defonce chat-input-key (reagent/atom 1))
|
||||
(defonce text-input-ref (reagent/atom nil))
|
||||
(defonce recording-audio? (reagent/atom false))
|
||||
(defonce reviewing-audio? (reagent/atom false))
|
||||
(defonce reviewing-audio-filepath (atom {}))
|
||||
(defonce record-audio-permission-granted (reagent/atom false))
|
||||
(defonce record-audio-reset-fn (atom nil))
|
||||
|
||||
(declare selectable-text-input)
|
||||
|
||||
(re-frame/reg-fx
|
||||
:chat.ui/clear-inputs
|
||||
(fn []
|
||||
(reset! input-texts {})
|
||||
(reset! input-text-content-heights {})
|
||||
(reset! mentions-enabled? {})
|
||||
(reset! chat-input-key 1)))
|
||||
|
||||
(defn input-focus
|
||||
[text-input-ref]
|
||||
(some-> ^js (quo.react/current-ref text-input-ref)
|
||||
.focus))
|
||||
|
||||
(defn show-send
|
||||
[{:keys [actions-ref send-ref sticker-ref record-ref]} chat-id]
|
||||
(when (and (or @recording-audio?
|
||||
(get @reviewing-audio-filepath chat-id))
|
||||
@record-audio-reset-fn)
|
||||
(@record-audio-reset-fn)
|
||||
(swap! reviewing-audio-filepath dissoc chat-id))
|
||||
(when actions-ref
|
||||
(quo.react/set-native-props actions-ref #js {:width 0 :left -88}))
|
||||
(quo.react/set-native-props send-ref #js {:width nil :right nil})
|
||||
(quo.react/set-native-props record-ref #js {:right nil :left -1000})
|
||||
(when sticker-ref
|
||||
(quo.react/set-native-props sticker-ref #js {:width 0 :right -100})))
|
||||
|
||||
(defn hide-send
|
||||
[{:keys [actions-ref send-ref sticker-ref record-ref]}]
|
||||
(when actions-ref
|
||||
(quo.react/set-native-props actions-ref #js {:width nil :left nil}))
|
||||
(quo.react/set-native-props send-ref #js {:width 0 :right -100})
|
||||
(quo.react/set-native-props record-ref #js {:left 0 :right 0})
|
||||
(when sticker-ref
|
||||
(quo.react/set-native-props sticker-ref #js {:width nil :right nil})))
|
||||
|
||||
(defn reset-input
|
||||
[refs chat-id]
|
||||
(some-> ^js (quo.react/current-ref (:text-input-ref refs))
|
||||
.clear)
|
||||
(swap! mentions-enabled? update :render not)
|
||||
(swap! input-texts dissoc chat-id)
|
||||
(swap! input-text-content-heights dissoc chat-id))
|
||||
|
||||
(defn clear-input
|
||||
[chat-id refs]
|
||||
(hide-send refs)
|
||||
(if (get @mentions-enabled? chat-id)
|
||||
(do
|
||||
(swap! mentions-enabled? dissoc chat-id)
|
||||
;;we need this timeout, because if we clear text input and first index was a mention object with
|
||||
;;blue color,
|
||||
;;after clearing text will be typed with this blue color, so we render white text first and then
|
||||
;;clear it
|
||||
(js/setTimeout #(reset-input refs chat-id) 50))
|
||||
(reset-input refs chat-id)))
|
||||
|
||||
(defn on-text-change
|
||||
[val chat-id]
|
||||
(swap! input-texts assoc chat-id val)
|
||||
;;we still store it in app-db for mentions, we don't have reactions in views
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text val]))
|
||||
|
||||
(defn on-selection-change
|
||||
[timeout-id last-text-change args]
|
||||
(let [selection (.-selection ^js (.-nativeEvent ^js args))
|
||||
start (.-start selection)
|
||||
end (.-end selection)]
|
||||
;; NOTE(rasom): on iOS we do not dispatch this event immediately
|
||||
;; because it is needed only in case if selection is changed without
|
||||
;; typing. Timeout might be canceled on `on-change`.
|
||||
(when platform/ios?
|
||||
(reset!
|
||||
timeout-id
|
||||
(background-timer/set-timeout
|
||||
#(rf/dispatch [:mention/on-selection-change
|
||||
{:start start
|
||||
:end end}])
|
||||
50)))
|
||||
;; NOTE(rasom): on Android we dispatch event only in case if there
|
||||
;; was no text changes during last 50ms. `on-selection-change` is
|
||||
;; dispatched after `on-change`, that's why there is no another way
|
||||
;; to know whether selection was changed without typing.
|
||||
(when (and platform/android?
|
||||
(or (not @last-text-change)
|
||||
(< 50 (- (js/Date.now) @last-text-change))))
|
||||
(rf/dispatch [:mention/on-selection-change
|
||||
{:start start
|
||||
:end end}]))))
|
||||
|
||||
(defn on-change
|
||||
[last-text-change timeout-id refs chat-id sending-image args]
|
||||
(let [text (.-text ^js (.-nativeEvent ^js args))
|
||||
prev-text (get @input-texts chat-id)]
|
||||
(when (and (seq prev-text) (empty? text) (not sending-image))
|
||||
(hide-send refs))
|
||||
(when (and (empty? prev-text) (or (seq text) sending-image))
|
||||
(show-send refs chat-id)
|
||||
(reset! recording-audio? false)
|
||||
(swap! reviewing-audio-filepath dissoc chat-id))
|
||||
|
||||
(when (and (not (get @mentions-enabled? chat-id)) (string/index-of text "@"))
|
||||
(swap! mentions-enabled? assoc chat-id true))
|
||||
|
||||
;; NOTE(rasom): on iOS `on-selection-change` is canceled in case if it
|
||||
;; happens during typing because it is not needed for mention
|
||||
;; suggestions calculation
|
||||
(when (and platform/ios? @timeout-id)
|
||||
(background-timer/clear-timeout @timeout-id))
|
||||
(when platform/android?
|
||||
(reset! last-text-change (js/Date.now)))
|
||||
|
||||
(on-text-change text chat-id)
|
||||
|
||||
(rf/dispatch [:mention/on-change-text text])))
|
||||
|
||||
(defn on-text-input
|
||||
[chat-id args]
|
||||
(let [native-event (.-nativeEvent ^js args)
|
||||
text (.-text ^js native-event)]
|
||||
(when (and (not (get @mentions-enabled? chat-id)) (string/index-of text "@"))
|
||||
(swap! mentions-enabled? assoc chat-id true))))
|
||||
|
||||
(defn text-input-style
|
||||
[chat-id]
|
||||
(merge typography/font-regular
|
||||
typography/paragraph-1
|
||||
{:flex 1
|
||||
:min-height 34
|
||||
:margin 0
|
||||
:flex-shrink 1
|
||||
:color (colors/theme-colors colors/neutral-100 colors/white)
|
||||
:margin-horizontal 20}
|
||||
(if platform/android?
|
||||
{:padding-vertical 8
|
||||
:text-align-vertical :top}
|
||||
{:margin-top 8
|
||||
:margin-bottom 8})
|
||||
(when (or @recording-audio? (get @reviewing-audio-filepath chat-id))
|
||||
{:display :none})))
|
||||
|
||||
(defn text-input
|
||||
[{:keys [refs chat-id sending-image on-content-size-change]}]
|
||||
(let [cooldown-enabled? (rf/sub [:chats/current-chat-cooldown-enabled?])
|
||||
timeout-id (reagent/atom nil)
|
||||
last-text-change (reagent/atom nil)
|
||||
mentions-enabled? (get @mentions-enabled? chat-id)
|
||||
props
|
||||
{:style (text-input-style chat-id)
|
||||
:ref (:text-input-ref refs)
|
||||
:max-font-size-multiplier 1
|
||||
:accessibility-label :chat-message-input
|
||||
:text-align-vertical :center
|
||||
:multiline true
|
||||
:editable (not cooldown-enabled?)
|
||||
:blur-on-submit false
|
||||
:auto-focus false
|
||||
:max-length chat.constants/max-text-size
|
||||
:placeholder-text-color (colors/theme-colors colors/neutral-40 colors/white-opa-30)
|
||||
:placeholder (if cooldown-enabled?
|
||||
(i18n/label :cooldown/text-input-disabled)
|
||||
(i18n/label :t/type-a-message))
|
||||
:default-value (get @input-texts chat-id)
|
||||
:underline-color-android :transparent
|
||||
:auto-capitalize :sentences
|
||||
:auto-correct false
|
||||
:spell-check false
|
||||
:on-content-size-change on-content-size-change
|
||||
:on-selection-change (partial on-selection-change
|
||||
timeout-id
|
||||
last-text-change)
|
||||
:on-change
|
||||
(partial on-change last-text-change timeout-id refs chat-id sending-image)
|
||||
:on-text-input (partial on-text-input chat-id)}
|
||||
input-with-mentions (rf/sub [:chat/input-with-mentions])
|
||||
children (fn []
|
||||
(if mentions-enabled?
|
||||
(map-indexed
|
||||
(fn [index [mention-type text]]
|
||||
^{:key (str index "_" mention-type "_" text)}
|
||||
[rn/text (when (= mention-type :mention) {:style {:color colors/primary-50}})
|
||||
text])
|
||||
input-with-mentions)
|
||||
(get @input-texts chat-id)))]
|
||||
(reset! text-input-ref (:text-input-ref refs))
|
||||
;when ios implementation for selectable-text-input is ready, we need remove this condition and use
|
||||
;selectable-text-input directly.
|
||||
(if platform/android?
|
||||
[selectable-text-input chat-id props children]
|
||||
[rn/text-input props
|
||||
children])))
|
||||
|
||||
(declare first-level-menu-items second-level-menu-items)
|
||||
|
||||
(defn update-input-text
|
||||
[{:keys [text-input chat-id]} text]
|
||||
(on-text-change text chat-id)
|
||||
(.setNativeProps ^js text-input (clj->js {:text text})))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:set-text-input-value
|
||||
(fn [[chat-id text local-text-input-ref]]
|
||||
(when local-text-input-ref
|
||||
(reset! text-input-ref local-text-input-ref))
|
||||
(on-text-change text chat-id)
|
||||
(if platform/ios?
|
||||
(.setNativeProps ^js (quo.react/current-ref @text-input-ref) (clj->js {:text text}))
|
||||
(if (string/blank? text)
|
||||
(.clear ^js (quo.react/current-ref @text-input-ref))
|
||||
(.setNativeProps ^js (quo.react/current-ref @text-input-ref) (clj->js {:text text}))))))
|
||||
|
||||
(defn calculate-input-text
|
||||
[{:keys [full-text selection-start selection-end]} content]
|
||||
(let [head (subs full-text 0 selection-start)
|
||||
tail (subs full-text selection-end)]
|
||||
(str head content tail)))
|
||||
|
||||
(defn update-selection
|
||||
[text-input-handle selection-start selection-end]
|
||||
;to avoid something disgusting like this
|
||||
;https://lightrun.com/answers/facebook-react-native-textinput-controlled-selection-broken-on-both-ios-and-android
|
||||
;use native invoke instead! do not use setNativeProps! e.g. (.setNativeProps ^js text-input (clj->js
|
||||
;{:selection {:start selection-start :end selection-end}}))
|
||||
(let [manager (rn/selectable-text-input-manager)]
|
||||
(oops/ocall manager :setSelection text-input-handle selection-start selection-end)))
|
||||
|
||||
(def first-level-menus
|
||||
{:cut (fn [{:keys [content] :as params}]
|
||||
(let [new-text (calculate-input-text params "")]
|
||||
(clipboard/set-string content)
|
||||
(update-input-text params new-text)))
|
||||
|
||||
:copy-to-clipboard (fn [{:keys [content]}]
|
||||
(clipboard/set-string content))
|
||||
|
||||
:paste (fn [params]
|
||||
(let [callback (fn [paste-content]
|
||||
(let [content (string/trim paste-content)
|
||||
new-text (calculate-input-text params content)]
|
||||
(update-input-text params new-text)))]
|
||||
(clipboard/get-string callback)))
|
||||
|
||||
:biu (fn [{:keys [first-level text-input-handle menu-items selection-start
|
||||
selection-end]}]
|
||||
(reset! first-level false)
|
||||
(reset! menu-items second-level-menu-items)
|
||||
(update-selection text-input-handle selection-start selection-end))})
|
||||
|
||||
(def first-level-menu-items (map i18n/label (keys first-level-menus)))
|
||||
|
||||
(defn reset-to-first-level-menu
|
||||
[first-level menu-items]
|
||||
(reset! first-level true)
|
||||
(reset! menu-items first-level-menu-items))
|
||||
|
||||
(defn append-markdown-char
|
||||
[{:keys [first-level menu-items content selection-start selection-end text-input-handle
|
||||
selection-event]
|
||||
:as params} wrap-chars]
|
||||
(let [content (str wrap-chars content wrap-chars)
|
||||
new-text (calculate-input-text params content)
|
||||
len-wrap-chars (count wrap-chars)
|
||||
selection-start (+ selection-start len-wrap-chars)
|
||||
selection-end (+ selection-end len-wrap-chars)]
|
||||
;don't update selection directly here, process it within on-selection-change instead
|
||||
;so that we can avoid java.lang.IndexOutOfBoundsException: setSpan..
|
||||
(reset! selection-event {:start selection-start
|
||||
:end selection-end
|
||||
:text-input-handle text-input-handle})
|
||||
(update-input-text params new-text)
|
||||
(reset-to-first-level-menu first-level menu-items)))
|
||||
|
||||
(def second-level-menus
|
||||
{:bold #(append-markdown-char % "**")
|
||||
|
||||
:italic #(append-markdown-char % "*")
|
||||
|
||||
:strikethrough #(append-markdown-char % "~~")})
|
||||
|
||||
(def second-level-menu-items (map i18n/label (keys second-level-menus)))
|
||||
|
||||
(defn on-menu-item-touched
|
||||
[{:keys [first-level event-type] :as params}]
|
||||
(let [menus (if @first-level first-level-menus second-level-menus)
|
||||
menu-item-key (nth (keys menus) event-type)
|
||||
action (get menus menu-item-key)]
|
||||
(action params)))
|
||||
|
||||
(defn selectable-text-input
|
||||
[_ _ _]
|
||||
(let [text-input-ref (reagent/atom nil)
|
||||
menu-items (reagent/atom first-level-menu-items)
|
||||
first-level (reagent/atom true)
|
||||
selection-event (atom nil)
|
||||
manager (rn/selectable-text-input-manager)]
|
||||
(reagent/create-class
|
||||
{:component-did-mount
|
||||
(fn [this]
|
||||
(when @text-input-ref
|
||||
(let [selectable-text-input-handle (rn/find-node-handle this)
|
||||
text-input-handle (rn/find-node-handle @text-input-ref)]
|
||||
(oops/ocall manager :setupMenuItems selectable-text-input-handle text-input-handle))))
|
||||
|
||||
:component-did-update (fn [_ _ _ _]
|
||||
(when (not @first-level)
|
||||
(let [text-input-handle (rn/find-node-handle @text-input-ref)]
|
||||
(oops/ocall manager :startActionMode text-input-handle))))
|
||||
|
||||
:reagent-render
|
||||
(fn [chat-id {:keys [style ref on-selection-change] :as props} children]
|
||||
(let [ref #(do (reset! text-input-ref %)
|
||||
(when ref
|
||||
(quo.react/set-ref-val! ref %)))
|
||||
on-selection-change (fn [args]
|
||||
(let [selection (.-selection ^js (.-nativeEvent ^js args))
|
||||
start (.-start selection)
|
||||
end (.-end selection)
|
||||
no-selection (<= (- end start) 0)]
|
||||
(when (and no-selection (not @first-level))
|
||||
(oops/ocall manager :hideLastActionMode)
|
||||
(reset-to-first-level-menu first-level menu-items)))
|
||||
(when on-selection-change
|
||||
(on-selection-change args))
|
||||
(when @selection-event
|
||||
(let [{:keys [start end text-input-handle]} @selection-event]
|
||||
(update-selection text-input-handle start end)
|
||||
(reset! selection-event nil))))
|
||||
on-selection
|
||||
(fn [^js event]
|
||||
(let [native-event (.-nativeEvent event)
|
||||
native-event (transforms/js->clj native-event)
|
||||
{:keys [eventType content selectionStart
|
||||
selectionEnd]} native-event
|
||||
full-text (:input-text (rf/sub [:chats/current-chat-input]))]
|
||||
(on-menu-item-touched {:first-level first-level
|
||||
:event-type eventType
|
||||
:content content
|
||||
:selection-start selectionStart
|
||||
:selection-end selectionEnd
|
||||
:text-input @text-input-ref
|
||||
:text-input-handle (rn/find-node-handle @text-input-ref)
|
||||
:full-text full-text
|
||||
:menu-items menu-items
|
||||
:chat-id chat-id
|
||||
:selection-event selection-event})))
|
||||
props (merge props
|
||||
{:ref ref
|
||||
:style (dissoc style :margin-horizontal)
|
||||
:on-selection-change on-selection-change
|
||||
:on-selection on-selection})]
|
||||
[rn/selectable-text-input {:menuItems @menu-items :style style}
|
||||
[rn/text-input props
|
||||
children]]))})))
|
|
@ -1,2 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.mentions)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
(ns status-im.ui2.screens.chat.composer.reply
|
||||
(:require [quo.react-native :as rn]
|
||||
[status-im.ui2.screens.chat.components.reply.view :as reply]
|
||||
[status-im.ui2.screens.chat.composer.input :as input]))
|
||||
|
||||
(defn focus-input-on-reply
|
||||
[reply had-reply text-input-ref]
|
||||
;;when we show reply we focus input
|
||||
(when-not (= reply @had-reply)
|
||||
(reset! had-reply reply)
|
||||
(when reply
|
||||
;; A setTimeout of 0 is necessary to ensure the statement is enqueued and will get executed ASAP.
|
||||
(js/setTimeout #(input/input-focus text-input-ref) 0))))
|
||||
|
||||
(defn reply-message-auto-focus-wrapper
|
||||
[text-input-ref _]
|
||||
(let [had-reply (atom nil)]
|
||||
(fn [_ reply]
|
||||
(focus-input-on-reply reply had-reply text-input-ref)
|
||||
(when reply
|
||||
[rn/view
|
||||
{:style (merge
|
||||
{:padding-horizontal 15
|
||||
:padding-vertical 8}
|
||||
(when @input/recording-audio?
|
||||
{:position :absolute
|
||||
:top 12
|
||||
:left 0
|
||||
:right 0
|
||||
;;When recording an audio and replying at the same time,
|
||||
;;text input is overlapped by the reply component but
|
||||
;;text input still have priority over touches, so we need
|
||||
;;to force the reply component to receive the touches in this
|
||||
;;scenario, thus we increase its z-index
|
||||
:z-index 1}))}
|
||||
[reply/reply-message reply true false
|
||||
(and @input/recording-audio? (not @input/reviewing-audio?))]]))))
|
|
@ -18,7 +18,6 @@
|
|||
[status-im.ui.screens.chat.styles.message.message :as style]
|
||||
[status-im.ui.screens.chat.utils :as chat.utils]
|
||||
[status-im.ui.screens.communities.icon :as communities.icon]
|
||||
[status-im.ui2.screens.chat.components.reply.view :as components.reply]
|
||||
[status-im.utils.utils :as utils]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.chat.home.chat-list-item.view :as home.chat-list-item]
|
||||
|
@ -144,9 +143,8 @@
|
|||
(:parsed-text content))))
|
||||
|
||||
(defn quoted-message
|
||||
[quoted-message pin?]
|
||||
[rn/view {:style (when-not pin? (style/quoted-message-container))}
|
||||
[components.reply/reply-message quoted-message false pin?]])
|
||||
[_ pin?]
|
||||
[rn/view {:style (when-not pin? (style/quoted-message-container))}])
|
||||
|
||||
(defn message-not-sent-text
|
||||
[chat-id message-id]
|
||||
|
|
|
@ -158,5 +158,3 @@
|
|||
["enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@test.waku.nodes.status.im"]})
|
||||
|
||||
(def default-kdf-iterations 3200)
|
||||
|
||||
(def ^:const new-composer-enabled? false)
|
||||
|
|
|
@ -263,7 +263,6 @@
|
|||
2 5000
|
||||
3 10000})
|
||||
|
||||
(def ^:const max-text-size 4096)
|
||||
;; any message that comes after this amount of ms will be grouped separately
|
||||
(def ^:const group-ms 300000)
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
(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)]])])])
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
(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}]]))])
|
|
@ -1,18 +0,0 @@
|
|||
(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)]])])
|
|
@ -1,172 +0,0 @@
|
|||
(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]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.style :as style]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.images.view :as images]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.reply.view :as reply]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.edit.view :as edit]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.mentions.view :as mentions]
|
||||
[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)
|
||||
:replying? (atom nil)
|
||||
:sending-images? (atom nil)
|
||||
:editing? (atom nil)
|
||||
:record-permission? (atom nil)
|
||||
:record-reset-fn (atom nil)
|
||||
:scroll-y (atom 0)}
|
||||
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)
|
||||
:recording? (reagent/atom false)}]
|
||||
[:f>
|
||||
(fn []
|
||||
(let [images (rf/sub [:chats/sending-image])
|
||||
audio (rf/sub [:chats/sending-audio])
|
||||
reply (rf/sub [:chats/reply-message])
|
||||
edit (rf/sub [:chats/edit-message])
|
||||
{: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
|
||||
(boolean (seq images))
|
||||
reply
|
||||
edit)
|
||||
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
|
||||
reply
|
||||
audio)
|
||||
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)
|
||||
cursor-pos (utils/cursor-y-position-relative-to-container
|
||||
props
|
||||
state)]
|
||||
(effects/initialize props
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
chat-input
|
||||
keyboard-height
|
||||
(boolean (seq images))
|
||||
reply
|
||||
edit
|
||||
audio)
|
||||
(utils/update-input props state input-text)
|
||||
[:<>
|
||||
[mentions/view props state animations max-height cursor-pos]
|
||||
[gesture/gesture-detector
|
||||
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
|
||||
[reanimated/view
|
||||
{:style (style/sheet-container insets state animations)
|
||||
:on-layout #(handler/layout % state blur-height)}
|
||||
[sub-view/bar]
|
||||
[reply/view state]
|
||||
[edit/view edit #(utils/cancel-edit-message state animations)]
|
||||
[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 reply)
|
||||
:on-content-size-change #(handler/content-size-change %
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
(or keyboard-shown edit))
|
||||
:on-scroll #(handler/scroll % props 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 props state)
|
||||
:accessibility-label :chat-message-input}]
|
||||
[gradients/view props state animations show-bottom-gradient?]]
|
||||
[images/images-list]
|
||||
[actions/view props state animations window-height insets
|
||||
(boolean (seq images))]]]]))]))])
|
||||
|
||||
(defn f-bottom-sheet-composer
|
||||
[insets]
|
||||
(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]]))
|
||||
|
||||
(defn bottom-sheet-composer
|
||||
[insets]
|
||||
[:f> f-bottom-sheet-composer insets])
|
|
@ -1,8 +1,8 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.actions.style
|
||||
(ns status-im2.contexts.chat.composer.actions.style
|
||||
(:require
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(def actions-container
|
||||
{:height constants/actions-container-height
|
|
@ -1,4 +1,4 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.actions.view
|
||||
(ns status-im2.contexts.chat.composer.actions.view
|
||||
(:require
|
||||
[quo2.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
|
@ -7,11 +7,11 @@
|
|||
[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.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]))
|
||||
[status-im2.contexts.chat.composer.actions.style :as style]))
|
||||
|
||||
(defn send-message
|
||||
[{:keys [text-value focused? maximized?]}
|
||||
|
@ -34,45 +34,45 @@
|
|||
(reset! text-value "")
|
||||
(messages.list/scroll-to-bottom))
|
||||
|
||||
(defn send-button
|
||||
(defn f-send-button
|
||||
[{: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 state animations window-height)}
|
||||
:i/arrow-up]])]))])
|
||||
animations window-height images?
|
||||
btn-opacity z-index]
|
||||
(rn/use-effect (fn []
|
||||
(if (or (seq @text-value) images?)
|
||||
(when (or (not= @z-index 1) (not= (reanimated/get-shared-value btn-opacity) 1))
|
||||
(reset! z-index 1)
|
||||
(js/setTimeout #(reanimated/animate btn-opacity 1) 50))
|
||||
(when (or (not= @z-index 0) (not= (reanimated/get-shared-value btn-opacity) 0))
|
||||
(reanimated/animate btn-opacity 0)
|
||||
(js/setTimeout #(when (and (empty? @text-value) (not images?)) (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 state animations window-height)}
|
||||
:i/arrow-up]])
|
||||
|
||||
(defn send-button
|
||||
[state animations window-height images?]
|
||||
(let [btn-opacity (reanimated/use-shared-value 0)
|
||||
z-index (reagent/atom 0)]
|
||||
[:f> f-send-button state animations window-height images? btn-opacity z-index]))
|
||||
|
||||
(defn audio-button
|
||||
[{:keys [record-permission? record-reset-fn]}
|
||||
{:keys [recording? gesture-enabled?]}
|
||||
[{:keys [record-reset-fn]}
|
||||
{:keys [record-permission? recording? gesture-enabled? focused?]}
|
||||
{:keys [container-opacity]}]
|
||||
(let [audio (rf/sub [:chats/sending-audio])]
|
||||
[rn/view
|
||||
{:style (style/record-audio-container)
|
||||
:pointer-events :box-none}
|
||||
[quo/record-audio
|
||||
{:record-audio-permission-granted record-permission?
|
||||
{:record-audio-permission-granted @record-permission?
|
||||
:on-init (fn [reset-fn]
|
||||
(reset! record-reset-fn reset-fn))
|
||||
:on-start-recording (fn []
|
||||
|
@ -86,15 +86,17 @@
|
|||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(rf/dispatch [:chat/send-audio file-path duration])
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity)
|
||||
(when-not @focused?
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil]))
|
||||
:on-cancel (fn []
|
||||
(when @recording?
|
||||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity)
|
||||
(when-not @focused?
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil])))
|
||||
:on-check-audio-permissions (fn []
|
||||
(permissions/permission-granted?
|
||||
|
@ -174,7 +176,7 @@
|
|||
:i/format])
|
||||
|
||||
(defn view
|
||||
[props state animations window-height insets images? audio]
|
||||
[props state animations window-height insets edit? images?]
|
||||
[rn/view {:style style/actions-container}
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
|
@ -183,5 +185,6 @@
|
|||
[image-button props animations insets]
|
||||
[reaction-button]
|
||||
[format-button]]
|
||||
[send-button state animations window-height images?]
|
||||
[audio-button props state animations audio]])
|
||||
[:f> send-button state animations window-height images?]
|
||||
(when (and (not edit?) (not images?))
|
||||
[audio-button props state animations])])
|
|
@ -1,17 +1,19 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.constants
|
||||
(ns status-im2.contexts.chat.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 input-height 32)
|
||||
|
||||
(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 line-height (:line-height typography/paragraph-1))
|
||||
|
||||
(def ^:const multiline-minimized-height (+ input-height (if platform/ios? 18 line-height)))
|
||||
|
||||
(def ^:const empty-opacity 0.7)
|
||||
|
||||
|
@ -23,14 +25,14 @@
|
|||
|
||||
(def ^:const mentions-max-height 240)
|
||||
|
||||
(def ^:const extra-content-offset (if platform/ios? 6 0))
|
||||
(def ^:const extra-content-offset (if platform/ios? 6 -8))
|
||||
|
||||
(def ^:const content-change-threshold 10)
|
||||
|
||||
(def ^:const drag-threshold 30)
|
||||
|
||||
(def ^:const velocity-threshold -1000)
|
||||
(def ^:const velocity-threshold (if platform/ios? -1000 -500))
|
||||
|
||||
(def ^:const background-threshold 0.75)
|
||||
|
||||
(def ^:const line-height (:line-height typography/paragraph-1))
|
||||
(def ^:const max-text-size 4096)
|
|
@ -1,4 +1,4 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.edit.style)
|
||||
(ns status-im2.contexts.chat.composer.edit.style)
|
||||
|
||||
(def container
|
||||
{:flex-direction :row
|
|
@ -1,16 +1,17 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.edit.view
|
||||
(ns status-im2.contexts.chat.composer.edit.view
|
||||
(:require
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[utils.i18n :as i18n]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[re-frame.core :as rf]
|
||||
[react-native.core :as rn]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.edit.style :as style]))
|
||||
[status-im2.contexts.chat.composer.edit.style :as style]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn edit-message
|
||||
[cancel]
|
||||
[state]
|
||||
[rn/view
|
||||
{:style style/container
|
||||
:accessibility-label :edit-message}
|
||||
|
@ -30,7 +31,7 @@
|
|||
:size 24
|
||||
:accessibility-label :edit-cancel-button
|
||||
:on-press (fn []
|
||||
(cancel)
|
||||
(utils/cancel-edit-message state)
|
||||
(rf/dispatch [:chat.ui/cancel-message-edit]))
|
||||
:type :outline}
|
||||
[quo/icon :i/close
|
||||
|
@ -38,12 +39,13 @@
|
|||
:color (colors/theme-colors colors/neutral-100 colors/neutral-40)}]]])
|
||||
|
||||
(defn- f-view
|
||||
[edit cancel]
|
||||
(let [height (reanimated/use-shared-value (if edit constants/edit-container-height 0))]
|
||||
[state]
|
||||
(let [edit (rf/sub [:chats/edit-message])
|
||||
height (reanimated/use-shared-value (if edit constants/edit-container-height 0))]
|
||||
(rn/use-effect #(reanimated/animate height (if edit constants/edit-container-height 0)) [edit])
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
|
||||
(when edit [edit-message cancel])]))
|
||||
(when edit [edit-message state])]))
|
||||
|
||||
(defn view
|
||||
[edit cancel]
|
||||
[:f> f-view edit cancel])
|
||||
[state]
|
||||
[:f> f-view state])
|
|
@ -1,11 +1,14 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.effects
|
||||
(ns status-im2.contexts.chat.composer.effects
|
||||
(:require
|
||||
[react-native.platform :as platform]
|
||||
[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.number :as utils.number]))
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.keyboard :as kb]
|
||||
[utils.number :as utils.number]
|
||||
[oops.core :as oops]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn reenter-screen-effect
|
||||
[{:keys [text-value saved-cursor-position maximized?]}
|
||||
|
@ -48,35 +51,17 @@
|
|||
(reanimated/set-shared-value background-y 0)
|
||||
(reanimated/animate opacity 1)))
|
||||
|
||||
(defn images-or-reply-effect
|
||||
[{:keys [container-opacity]}
|
||||
{:keys [replying? sending-images? input-ref]}
|
||||
images? reply?]
|
||||
(when (or images? reply?)
|
||||
(defn images-effect
|
||||
[{:keys [sending-images? input-ref]}
|
||||
{:keys [container-opacity]}
|
||||
images?]
|
||||
(when images?
|
||||
(reanimated/animate container-opacity 1))
|
||||
(when (and (not @sending-images?) images? @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
(reset! sending-images? true))
|
||||
(when (and (not @replying?) reply? @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
(reset! replying? true))
|
||||
(when-not images?
|
||||
(reset! sending-images? false))
|
||||
(when-not reply?
|
||||
(reset! replying? false)))
|
||||
|
||||
(defn edit-effect
|
||||
[{:keys [text-value saved-cursor-position]}
|
||||
{:keys [editing? input-ref]}
|
||||
edit]
|
||||
(let [edit-text (get-in edit [:content :text])]
|
||||
(when (and (not @editing?) edit @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
(reset! editing? true)
|
||||
(reset! text-value edit-text)
|
||||
(reset! saved-cursor-position (count edit-text)))
|
||||
(when-not edit-text
|
||||
(reset! editing? false))))
|
||||
(reset! sending-images? false)))
|
||||
|
||||
(defn audio-effect
|
||||
[{:keys [recording? gesture-enabled?]}
|
||||
|
@ -105,7 +90,7 @@
|
|||
|
||||
(defn initialize
|
||||
[props state animations {:keys [max-height] :as dimensions} chat-input keyboard-height images? reply?
|
||||
edit audio]
|
||||
audio]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(maximized-effect state animations dimensions chat-input)
|
||||
|
@ -113,10 +98,77 @@
|
|||
(layout-effect state)
|
||||
(kb-default-height-effect state)
|
||||
(background-effect state animations dimensions chat-input)
|
||||
(images-or-reply-effect animations props images? reply?)
|
||||
(edit-effect state props edit)
|
||||
(images-effect props animations images?)
|
||||
(audio-effect state animations audio)
|
||||
(empty-effect state animations images? reply? audio)
|
||||
(kb/add-kb-listeners props state animations dimensions keyboard-height)
|
||||
#(component-will-unmount props))
|
||||
[max-height]))
|
||||
|
||||
(defn edit
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value saved-cursor-position]}
|
||||
edit]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(let [edit-text (get-in edit [:content :text])]
|
||||
(when (and edit @input-ref)
|
||||
(.focus ^js @input-ref)
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text edit-text}))
|
||||
(reset! text-value edit-text)
|
||||
(reset! saved-cursor-position (count edit-text)))))
|
||||
[(:message-id edit)]))
|
||||
|
||||
(defn reply
|
||||
[{:keys [input-ref]}
|
||||
{:keys [container-opacity]}
|
||||
reply]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when reply
|
||||
(reanimated/animate container-opacity 1))
|
||||
(when (and reply @input-ref)
|
||||
(.focus ^js @input-ref)))
|
||||
[(:message-id reply)]))
|
||||
|
||||
(defn edit-mentions
|
||||
[{:keys [input-ref]} {:keys [text-value cursor-position]} input-with-mentions]
|
||||
(rn/use-effect (fn []
|
||||
(let [input-text (reduce (fn [acc item]
|
||||
(str acc (second item)))
|
||||
""
|
||||
input-with-mentions)]
|
||||
(reset! text-value input-text)
|
||||
(reset! cursor-position (count input-text))
|
||||
(js/setTimeout #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
||||
(clj->js {:selection {:start (count input-text)
|
||||
:end (count
|
||||
input-text)}})))
|
||||
300)))
|
||||
[(some #(= :mention (first %)) (seq input-with-mentions))]))
|
||||
|
||||
(defn update-input-mention
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value]}
|
||||
input-text]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (and input-text (not= @text-value input-text))
|
||||
(when @input-ref
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text input-text})))
|
||||
(reset! text-value input-text)
|
||||
(rf/dispatch [:mention/on-change-text input-text])))
|
||||
[input-text]))
|
||||
|
||||
(defn did-mount
|
||||
[{:keys [selectable-input-ref input-ref selection-manager]}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when platform/android?
|
||||
(let [selectable-text-input-handle (rn/find-node-handle @selectable-input-ref)
|
||||
text-input-handle (rn/find-node-handle @input-ref)]
|
||||
(oops/ocall selection-manager
|
||||
:setupMenuItems
|
||||
selectable-text-input-handle
|
||||
text-input-handle))))))
|
|
@ -1,10 +1,10 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.gesture
|
||||
(ns status-im2.contexts.chat.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]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn set-opacity
|
||||
|
@ -35,10 +35,12 @@
|
|||
(rf/dispatch [:chat.ui/set-input-maximized true]))
|
||||
|
||||
(defn minimize
|
||||
[{:keys [input-ref emoji-kb-extra-height saved-emoji-kb-extra-height]}]
|
||||
[{:keys [input-ref emoji-kb-extra-height saved-emoji-kb-extra-height]}
|
||||
{:keys [maximized?]}]
|
||||
(when @emoji-kb-extra-height
|
||||
(reset! saved-emoji-kb-extra-height @emoji-kb-extra-height)
|
||||
(reset! emoji-kb-extra-height nil))
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])
|
||||
(when @input-ref
|
||||
(.blur ^js @input-ref)))
|
||||
|
@ -74,21 +76,26 @@
|
|||
(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-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)
|
||||
bounded-height (utils/bounded-val new-height min-height max-height)]
|
||||
(when keyboard-shown
|
||||
(if (>= new-height min-height)
|
||||
(do ; expand sheet
|
||||
(reanimated/set-shared-value height bounded-height)
|
||||
(set-opacity (oops/oget event "velocityY")
|
||||
opacity
|
||||
translation
|
||||
@expanding?
|
||||
min-height
|
||||
max-height
|
||||
bounded-height
|
||||
saved-height))
|
||||
(when @input-ref ; sheet at min-height, collapse keyboard
|
||||
(.blur ^js @input-ref)))))))
|
||||
(gesture/on-end (fn []
|
||||
(let [diff (- (reanimated/get-shared-value height)
|
||||
(reanimated/get-shared-value saved-height))]
|
||||
|
@ -98,6 +105,6 @@
|
|||
(maximize state animations dimensions)
|
||||
(bounce-back animations dimensions starting-opacity))
|
||||
(if (> (Math/abs diff) constants/drag-threshold)
|
||||
(minimize props)
|
||||
(minimize props state)
|
||||
(bounce-back animations dimensions starting-opacity)))
|
||||
(reset! gesture-enabled? true))))))))
|
|
@ -1,8 +1,7 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.gradients.style
|
||||
(ns status-im2.contexts.chat.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
|
||||
|
@ -25,7 +24,7 @@
|
|||
:style (top-gradient-style opacity z-index)})
|
||||
|
||||
(def bottom-gradient-style
|
||||
{:height (if platform/ios? (:line-height typography/paragraph-1) 32)
|
||||
{:height (:line-height typography/paragraph-1)
|
||||
:position :absolute
|
||||
:bottom 0
|
||||
:left 0
|
||||
|
@ -39,4 +38,3 @@
|
|||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style bottom-gradient-style})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
(ns status-im2.contexts.chat.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.composer.gradients.style :as style]))
|
||||
|
||||
|
||||
(defn f-view
|
||||
[{:keys [input-ref]}
|
||||
{:keys [gradient-z-index]}
|
||||
{:keys [gradient-opacity]}
|
||||
show-bottom-gradient?]
|
||||
[:<>
|
||||
[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)]])])
|
||||
(defn view
|
||||
[props state animations show-bottom-gradient?]
|
||||
[:f> f-view props state animations show-bottom-gradient?])
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.handlers
|
||||
(ns status-im2.contexts.chat.composer.handlers
|
||||
(:require
|
||||
[react-native.core :as rn]
|
||||
[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]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.keyboard :as kb]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[status-im2.contexts.chat.composer.selection :as selection]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn focus
|
||||
|
@ -54,7 +56,8 @@
|
|||
(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))))
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false]))))
|
||||
|
||||
(defn content-size-change
|
||||
[event
|
||||
|
@ -113,13 +116,26 @@
|
|||
(rf/dispatch [:mention/on-change-text text]))
|
||||
|
||||
(defn selection-change
|
||||
[event {:keys [lock-selection? cursor-position]}]
|
||||
(when-not @lock-selection?
|
||||
(reset! cursor-position (oops/oget event "nativeEvent.selection.end"))))
|
||||
[event
|
||||
{:keys [input-ref selection-event selection-manager]}
|
||||
{:keys [lock-selection? cursor-position first-level? menu-items]}]
|
||||
(let [start (oops/oget event "nativeEvent.selection.start")
|
||||
end (oops/oget event "nativeEvent.selection.end")
|
||||
selection? (not= start end)
|
||||
text-input-handle (rn/find-node-handle @input-ref)]
|
||||
(when-not @lock-selection?
|
||||
(reset! cursor-position end))
|
||||
(when (and selection? (not @first-level?))
|
||||
(js/setTimeout #(oops/ocall selection-manager :startActionMode text-input-handle) 500))
|
||||
(when (and (not selection?) (not @first-level?))
|
||||
(oops/ocall selection-manager :hideLastActionMode)
|
||||
(selection/reset-to-first-level-menu first-level? menu-items))
|
||||
(when @selection-event
|
||||
(let [{:keys [start end text-input-handle]} @selection-event]
|
||||
(selection/update-selection text-input-handle start end)
|
||||
(reset! selection-event nil)))))
|
||||
|
||||
(defn layout
|
||||
[event
|
||||
{:keys [lock-layout?]}
|
||||
blur-height]
|
||||
(when (utils/update-blur-height? event lock-layout? blur-height)
|
||||
[event state blur-height]
|
||||
(when (utils/update-blur-height? event state blur-height)
|
||||
(reanimated/set-shared-value blur-height (oops/oget event "nativeEvent.layout.height"))))
|
|
@ -1,4 +1,4 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.images.style
|
||||
(ns status-im2.contexts.chat.composer.images.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
|
||||
(def image-container
|
|
@ -0,0 +1,47 @@
|
|||
(ns status-im2.contexts.chat.composer.images.view
|
||||
(:require [quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.images.style :as style]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.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 f-images-list
|
||||
[]
|
||||
(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} {:margin-horizontal -20})}
|
||||
[gesture/flat-list
|
||||
{:key-fn first
|
||||
:render-fn image
|
||||
:data images
|
||||
:content-container-style {:padding-horizontal 20}
|
||||
:horizontal true
|
||||
:shows-horizontal-scroll-indicator false
|
||||
:keyboard-should-persist-taps :handled}]]))
|
||||
|
||||
(defn images-list
|
||||
[]
|
||||
[:f> f-images-list])
|
|
@ -1,4 +1,4 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.keyboard
|
||||
(ns status-im2.contexts.chat.composer.keyboard
|
||||
(:require [oops.core :as oops]
|
||||
[status-im.async-storage.core :as async-storage]
|
||||
[react-native.core :as rn]
|
|
@ -1,8 +1,8 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.mentions.style
|
||||
(ns status-im2.contexts.chat.composer.mentions.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
|
||||
(defn shadow
|
|
@ -1,20 +1,20 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.mentions.view
|
||||
(ns status-im2.contexts.chat.composer.mentions.view
|
||||
(:require
|
||||
[react-native.hooks :as hooks]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[utils.re-frame :as rf]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.common.contact-list-item.view :as contact-list-item]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.mentions.style :as style]))
|
||||
[status-im2.contexts.chat.composer.mentions.style :as style]))
|
||||
|
||||
(defn update-cursor
|
||||
[user {:keys [cursor-position input-ref]}]
|
||||
(when platform/android?
|
||||
(let [new-cursor-pos (+ (count (:primary-name user)) @cursor-position)]
|
||||
(let [new-cursor-pos (+ (count (:primary-name user)) @cursor-position 1)]
|
||||
(reset! cursor-position new-cursor-pos)
|
||||
(reagent/next-tick #(when @input-ref
|
||||
(.setNativeProps ^js @input-ref
|
|
@ -1,5 +1,12 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.reply.style)
|
||||
(ns status-im2.contexts.chat.composer.reply.style)
|
||||
|
||||
|
||||
(defn container
|
||||
[pin? in-chat-input?]
|
||||
{:flex-direction :row
|
||||
:height (when-not pin? 24)
|
||||
:margin-left (when (and (not in-chat-input?) (not pin?)) 26)
|
||||
:margin-bottom (when (and (not in-chat-input?) (not pin?)) 8)})
|
||||
(defn reply-content
|
||||
[pin?]
|
||||
{:padding-right (when-not pin? 10)
|
|
@ -1,15 +1,15 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.reply.view
|
||||
(ns status-im2.contexts.chat.composer.reply.view
|
||||
(:require [clojure.string :as string]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[utils.i18n :as i18n]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[status-im2.constants :as constant]
|
||||
[status-im.ethereum.stateofus :as stateofus]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.reply.style :as style]
|
||||
[status-im2.contexts.chat.composer.reply.style :as style]
|
||||
[react-native.linear-gradient :as linear-gradient]))
|
||||
|
||||
(defn get-quoted-text-with-mentions
|
||||
|
@ -78,7 +78,7 @@
|
|||
:style style/message-author-text}
|
||||
(format-reply-author from contact-name current-public-key)]]))
|
||||
|
||||
(defn reply-message
|
||||
(defn quoted-message
|
||||
[{:keys [from identicon content-type contentType parsed-text content deleted? deleted-for-me?
|
||||
album-images-count]}
|
||||
in-chat-input? pin? recording-audio?]
|
||||
|
@ -86,9 +86,8 @@
|
|||
current-public-key (rf/sub [:multiaccount/public-key])
|
||||
content-type (or content-type contentType)]
|
||||
[rn/view
|
||||
{:style {:flex-direction :row
|
||||
:height (when-not pin? 24)
|
||||
:accessibility-label :reply-message}}
|
||||
{:style (style/container pin? in-chat-input?)
|
||||
:accessibility-label :reply-message}
|
||||
[rn/view {:style (style/reply-content pin?)}
|
||||
(when-not pin?
|
||||
[quo/icon :i/connector
|
||||
|
@ -148,7 +147,7 @@
|
|||
height (reanimated/use-shared-value (if reply constants/reply-container-height 0))]
|
||||
(rn/use-effect #(reanimated/animate height (if reply constants/reply-container-height 0)) [reply])
|
||||
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
|
||||
(when reply [reply-message reply true false recording?])]))
|
||||
(when reply [quoted-message reply true false recording?])]))
|
||||
|
||||
(defn view
|
||||
[{:keys [recording?]}]
|
|
@ -0,0 +1,109 @@
|
|||
(ns status-im2.contexts.chat.composer.selection
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[react-native.core :as rn]
|
||||
[oops.core :as oops]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(declare first-level-menu-items second-level-menu-items)
|
||||
|
||||
(defn update-input-text
|
||||
[{:keys [text-input]} text]
|
||||
(rf/dispatch [:chat.ui/set-chat-input-text text])
|
||||
(.setNativeProps ^js text-input (clj->js {:text text})))
|
||||
|
||||
(defn calculate-input-text
|
||||
[{:keys [full-text selection-start selection-end]} content]
|
||||
(let [head (subs full-text 0 selection-start)
|
||||
tail (subs full-text selection-end)]
|
||||
(str head content tail)))
|
||||
|
||||
(defn update-selection
|
||||
[text-input-handle selection-start selection-end]
|
||||
;; to avoid something like this
|
||||
;; https://lightrun.com/answers/facebook-react-native-textinput-controlled-selection-broken-on-both-ios-and-android
|
||||
;; use native invoke instead! do not use setNativeProps! e.g. (.setNativeProps ^js text-input (clj->js
|
||||
;; {:selection {:start selection-start :end selection-end}}))
|
||||
(let [manager (rn/selectable-text-input-manager)]
|
||||
(oops/ocall manager :setSelection text-input-handle selection-start selection-end)))
|
||||
|
||||
(defn reset-to-first-level-menu
|
||||
[first-level? menu-items]
|
||||
(reset! first-level? true)
|
||||
(reset! menu-items first-level-menu-items))
|
||||
|
||||
(defn append-markdown-char
|
||||
[{:keys [first-level? menu-items content selection-start selection-end text-input-handle
|
||||
selection-event]
|
||||
:as params} wrap-chars]
|
||||
(let [content (str wrap-chars content wrap-chars)
|
||||
new-text (calculate-input-text params content)
|
||||
len-wrap-chars (count wrap-chars)
|
||||
selection-start (+ selection-start len-wrap-chars)
|
||||
selection-end (+ selection-end len-wrap-chars)]
|
||||
;; don't update selection directly here, process it within on-selection-change instead
|
||||
;; so that we can avoid java.lang.IndexOutOfBoundsException: setSpan..
|
||||
(reset! selection-event {:start selection-start
|
||||
:end selection-end
|
||||
:text-input-handle text-input-handle})
|
||||
(update-input-text params new-text)
|
||||
(reset-to-first-level-menu first-level? menu-items)))
|
||||
|
||||
(def first-level-menus
|
||||
{:cut (fn [{:keys [content] :as params}]
|
||||
(let [new-text (calculate-input-text params "")]
|
||||
(clipboard/set-string content)
|
||||
(update-input-text params new-text)))
|
||||
|
||||
:copy-to-clipboard (fn [{:keys [content]}]
|
||||
(clipboard/set-string content))
|
||||
|
||||
:paste (fn [params]
|
||||
(let [callback (fn [paste-content]
|
||||
(let [content (string/trim paste-content)
|
||||
new-text (calculate-input-text params content)]
|
||||
(update-input-text params new-text)))]
|
||||
(clipboard/get-string callback)))
|
||||
|
||||
:biu (fn [{:keys [first-level? text-input-handle menu-items selection-start
|
||||
selection-end]}]
|
||||
(reset! first-level? false)
|
||||
(reset! menu-items second-level-menu-items)
|
||||
(update-selection text-input-handle selection-start selection-end))})
|
||||
|
||||
(def first-level-menu-items (map i18n/label (keys first-level-menus)))
|
||||
(def second-level-menus
|
||||
{:bold #(append-markdown-char % "**")
|
||||
|
||||
:italic #(append-markdown-char % "*")
|
||||
|
||||
:strikethrough #(append-markdown-char % "~~")})
|
||||
|
||||
(def second-level-menu-items (map i18n/label (keys second-level-menus)))
|
||||
|
||||
(defn on-menu-item-touched
|
||||
[{:keys [first-level? event-type] :as params}]
|
||||
(let [menus (if @first-level? first-level-menus second-level-menus)
|
||||
menu-item-key (nth (keys menus) event-type)
|
||||
action (get menus menu-item-key)]
|
||||
(action params)))
|
||||
|
||||
(defn on-selection
|
||||
[event
|
||||
{:keys [input-ref selection-event]}
|
||||
{:keys [first-level? menu-items]}]
|
||||
(let [{:strs [eventType content selectionStart
|
||||
selectionEnd]} (js->clj (oops/oget event "nativeEvent"))
|
||||
full-text (:input-text (rf/sub [:chats/current-chat-input]))]
|
||||
(on-menu-item-touched {:first-level? first-level?
|
||||
:event-type eventType
|
||||
:content content
|
||||
:selection-start selectionStart
|
||||
:selection-end selectionEnd
|
||||
:text-input @input-ref
|
||||
:text-input-handle (rn/find-node-handle @input-ref)
|
||||
:full-text full-text
|
||||
:menu-items menu-items
|
||||
:selection-event selection-event})))
|
|
@ -1,9 +1,9 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.style
|
||||
(ns status-im2.contexts.chat.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]))
|
||||
[status-im2.contexts.chat.composer.constants :as constants]))
|
||||
|
||||
(defn shadow
|
||||
[focused?]
|
||||
|
@ -54,20 +54,23 @@
|
|||
{:max-height max-height
|
||||
:overflow :hidden}))
|
||||
|
||||
(defn input
|
||||
(defn input-view
|
||||
[{:keys [saved-emoji-kb-extra-height]}
|
||||
{:keys [focused? recording?]}]
|
||||
{:z-index 1
|
||||
:position (if @saved-emoji-kb-extra-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or focused? platform/ios?) 0)
|
||||
:flex 1
|
||||
:display (if @recording? :none :flex)
|
||||
:min-height constants/input-height})
|
||||
|
||||
(defn input-text
|
||||
[]
|
||||
(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-emoji-kb-extra-height :relative :absolute)
|
||||
:top 0
|
||||
:left 0
|
||||
:right (when (or focused? platform/ios?) 0)
|
||||
:display (if @recording? :none :flex)}))
|
||||
{:color (colors/theme-colors :black :white)
|
||||
:text-align-vertical :top}))
|
||||
(defn background
|
||||
[opacity background-y window-height]
|
||||
(reanimated/apply-animations-to-style
|
||||
|
@ -82,11 +85,11 @@
|
|||
:z-index 1}))
|
||||
|
||||
(defn blur-container
|
||||
[height]
|
||||
[height focused?]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:height height}
|
||||
{:position :absolute
|
||||
:elevation 10
|
||||
:elevation (if-not @focused? 10 0)
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
|
@ -0,0 +1,19 @@
|
|||
(ns status-im2.contexts.chat.composer.sub-view
|
||||
(:require
|
||||
[react-native.blur :as blur]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.composer.style :as style]))
|
||||
|
||||
(defn bar
|
||||
[]
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style (style/bar)}]])
|
||||
|
||||
(defn f-blur-view
|
||||
[layout-height focused?]
|
||||
[reanimated/view {:style (style/blur-container layout-height focused?)}
|
||||
[blur/view (style/blur-view)]])
|
||||
(defn blur-view
|
||||
[layout-height focused?]
|
||||
[:f> f-blur-view layout-height focused?])
|
|
@ -1,10 +1,13 @@
|
|||
(ns status-im2.contexts.chat.bottom-sheet-composer.utils
|
||||
(ns status-im2.contexts.chat.composer.utils
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.selection :as selection]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn bounded-val
|
||||
|
@ -52,8 +55,9 @@
|
|||
(> new-height (* constants/background-threshold max-height))))
|
||||
|
||||
(defn update-blur-height?
|
||||
[event lock-layout? layout-height]
|
||||
[event {:keys [lock-layout? focused?]} layout-height]
|
||||
(or (not @lock-layout?)
|
||||
(not @focused?)
|
||||
(> (reanimated/get-shared-value layout-height) (oops/oget event "nativeEvent.layout.height"))))
|
||||
|
||||
(defn calc-lines
|
||||
|
@ -83,24 +87,10 @@
|
|||
(or (> lines 1) (seq images) reply? edit?))
|
||||
|
||||
(defn cancel-edit-message
|
||||
[{:keys [text-value maximized?]}
|
||||
{:keys [height saved-height last-height]}]
|
||||
(when-not @maximized?
|
||||
(reanimated/animate height constants/input-height)
|
||||
(reanimated/set-shared-value saved-height constants/input-height)
|
||||
(reanimated/set-shared-value last-height constants/input-height))
|
||||
[{:keys [text-value]}]
|
||||
(reset! text-value "")
|
||||
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height]))
|
||||
|
||||
(defn update-input
|
||||
[{:keys [input-ref]}
|
||||
{:keys [text-value]}
|
||||
input-text]
|
||||
(when (and input-text (not= @text-value input-text))
|
||||
(reset! text-value input-text)
|
||||
(when @input-ref
|
||||
(.setNativeProps ^js @input-ref (clj->js {:text input-text})))))
|
||||
|
||||
(defn count-lines
|
||||
[s]
|
||||
(-> s
|
||||
|
@ -141,3 +131,61 @@
|
|||
(if (< (+ base container-height) view-height)
|
||||
base
|
||||
(+ constants/actions-container-height (:bottom insets) (- curr-height cursor-pos) 18)))))
|
||||
|
||||
|
||||
(defn init-props
|
||||
[]
|
||||
{:input-ref (atom nil)
|
||||
:selectable-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)
|
||||
:sending-images? (atom false)
|
||||
:record-reset-fn (atom nil)
|
||||
:scroll-y (atom 0)
|
||||
:selection-event (atom nil)
|
||||
:selection-manager (rn/selectable-text-input-manager)})
|
||||
|
||||
(defn init-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)
|
||||
:record-permission? (reagent/atom true)
|
||||
:recording? (reagent/atom false)
|
||||
:first-level? (reagent/atom true)
|
||||
:menu-items (reagent/atom selection/first-level-menu-items)})
|
||||
(defn init-animations
|
||||
[lines input-text images reply audio content-height max-height opacity background-y]
|
||||
(let [initial-height (if (> lines 1)
|
||||
constants/multiline-minimized-height
|
||||
constants/input-height)]
|
||||
{:gradient-opacity (reanimated/use-shared-value 0)
|
||||
:container-opacity (reanimated/use-shared-value
|
||||
(if (empty-input?
|
||||
input-text
|
||||
images
|
||||
reply
|
||||
audio)
|
||||
0.7
|
||||
1))
|
||||
:height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:saved-height (reanimated/use-shared-value
|
||||
initial-height)
|
||||
:last-height (reanimated/use-shared-value
|
||||
(bounded-val
|
||||
@content-height
|
||||
constants/input-height
|
||||
max-height))
|
||||
:opacity opacity
|
||||
:background-y background-y}))
|
|
@ -0,0 +1,144 @@
|
|||
(ns status-im2.contexts.chat.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]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.composer.style :as style]
|
||||
[status-im2.contexts.chat.composer.images.view :as images]
|
||||
[status-im2.contexts.chat.composer.reply.view :as reply]
|
||||
[status-im2.contexts.chat.composer.edit.view :as edit]
|
||||
[status-im2.contexts.chat.composer.mentions.view :as mentions]
|
||||
[status-im2.contexts.chat.composer.utils :as utils]
|
||||
[status-im2.contexts.chat.composer.constants :as constants]
|
||||
[status-im2.contexts.chat.composer.actions.view :as actions]
|
||||
[status-im2.contexts.chat.composer.keyboard :as kb]
|
||||
[status-im2.contexts.chat.composer.sub-view :as sub-view]
|
||||
[status-im2.contexts.chat.composer.effects :as effects]
|
||||
[status-im2.contexts.chat.composer.gesture :as drag-gesture]
|
||||
[status-im2.contexts.chat.composer.handlers :as handler]
|
||||
[status-im2.contexts.chat.composer.gradients.view :as gradients]
|
||||
[status-im2.contexts.chat.composer.selection :as selection]))
|
||||
|
||||
(defn sheet-component
|
||||
[{:keys [insets window-height blur-height opacity background-y]} props state]
|
||||
(let [images (rf/sub [:chats/sending-image])
|
||||
audio (rf/sub [:chats/sending-audio])
|
||||
reply (rf/sub [:chats/reply-message])
|
||||
edit (rf/sub [:chats/edit-message])
|
||||
input-with-mentions (rf/sub [:chat/input-with-mentions])
|
||||
{: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
|
||||
(boolean (seq images))
|
||||
reply
|
||||
edit)
|
||||
lines (utils/calc-lines @content-height)
|
||||
max-lines (utils/calc-lines max-height)
|
||||
animations (utils/init-animations
|
||||
lines
|
||||
input-text
|
||||
images
|
||||
reply
|
||||
audio
|
||||
content-height
|
||||
max-height
|
||||
opacity
|
||||
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)
|
||||
cursor-pos (utils/cursor-y-position-relative-to-container props
|
||||
state)]
|
||||
(effects/did-mount props)
|
||||
(effects/initialize props
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
chat-input
|
||||
keyboard-height
|
||||
(boolean (seq images))
|
||||
reply
|
||||
audio)
|
||||
(effects/edit props state edit)
|
||||
(effects/reply props animations reply)
|
||||
(effects/update-input-mention props state input-text)
|
||||
(effects/edit-mentions props state input-with-mentions)
|
||||
[:<>
|
||||
[mentions/view props state animations max-height cursor-pos]
|
||||
[gesture/gesture-detector
|
||||
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
|
||||
[reanimated/view
|
||||
{:style (style/sheet-container insets state animations)
|
||||
:on-layout #(handler/layout % state blur-height)}
|
||||
[sub-view/bar]
|
||||
[reply/view state]
|
||||
[edit/view state]
|
||||
[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/selectable-text-input
|
||||
{:ref #(reset! (:selectable-input-ref props) %)
|
||||
:menu-items @(:menu-items state)
|
||||
:style (style/input-view props state)}
|
||||
[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 reply)
|
||||
:on-content-size-change #(handler/content-size-change %
|
||||
state
|
||||
animations
|
||||
dimensions
|
||||
(or keyboard-shown edit))
|
||||
:on-scroll #(handler/scroll % props state animations dimensions)
|
||||
:on-change-text #(handler/change-text % props state)
|
||||
:on-selection-change #(handler/selection-change % props state)
|
||||
:on-selection #(selection/on-selection % props 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-text)
|
||||
:max-length constants/max-text-size
|
||||
:accessibility-label :chat-message-input}]]
|
||||
[gradients/view props state animations show-bottom-gradient?]]
|
||||
[images/images-list]
|
||||
[actions/view props state animations window-height insets edit
|
||||
(boolean (seq images))]]]]))
|
||||
|
||||
(defn composer
|
||||
[insets]
|
||||
(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)))
|
||||
extra-params {:insets insets
|
||||
:window-height window-height
|
||||
:blur-height blur-height
|
||||
:opacity opacity
|
||||
:background-y background-y}
|
||||
props (utils/init-props)
|
||||
state (utils/init-state)]
|
||||
[rn/view
|
||||
[reanimated/view {:style (style/background opacity background-y window-height)}]
|
||||
[sub-view/blur-view blur-height (:focused? state)]
|
||||
[:f> sheet-component extra-params props state]]))
|
|
@ -1,28 +0,0 @@
|
|||
(ns status-im2.contexts.chat.messages.composer.controls.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
|
||||
(defn controls
|
||||
[bottom-inset]
|
||||
{:padding-horizontal 20
|
||||
:z-index 3
|
||||
:position :absolute
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-90)
|
||||
:padding-bottom (+ 12 bottom-inset)
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0})
|
||||
|
||||
(def buttons-container
|
||||
{:flex-direction :row
|
||||
:margin-top 12
|
||||
:min-height 32})
|
||||
|
||||
(defn record-audio-container
|
||||
[bottom-inset]
|
||||
{:align-items :center
|
||||
:background-color :transparent
|
||||
:flex-direction :row
|
||||
:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom (- bottom-inset 7)})
|
|
@ -1,127 +0,0 @@
|
|||
(ns status-im2.contexts.chat.messages.composer.controls.view
|
||||
(:require [react-native.core :as rn]
|
||||
[react-native.background-timer :as background-timer]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.i18n :as i18n]
|
||||
[quo2.core :as quo]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
[status-im2.contexts.chat.messages.composer.controls.style :as style]
|
||||
[status-im2.contexts.chat.messages.list.view :as messages.list]
|
||||
[status-im.ui2.screens.chat.composer.images.view :as composer-images]
|
||||
[status-im.utils.utils :as utils-old]
|
||||
[status-im.ui2.screens.chat.composer.input :as input]
|
||||
[status-im2.common.alert.events :as alert]
|
||||
[react-native.permissions :as permissions]
|
||||
[quo.react :as quo.react]))
|
||||
|
||||
(defn send-button
|
||||
[send-ref {:keys [chat-id images]} on-send]
|
||||
[rn/view
|
||||
{:ref send-ref
|
||||
:style (when-not (or (seq (get @input/input-texts chat-id)) (seq images))
|
||||
{:width 0
|
||||
:right -100})}
|
||||
[quo/button
|
||||
{:icon true
|
||||
:size 32
|
||||
:accessibility-label :send-message-button
|
||||
:on-press (fn []
|
||||
(on-send)
|
||||
(messages.list/scroll-to-bottom)
|
||||
(rf/dispatch [:chat.ui/send-current-message]))}
|
||||
:i/arrow-up]])
|
||||
|
||||
(defn reactions-button
|
||||
[]
|
||||
[not-implemented/not-implemented
|
||||
[quo/button
|
||||
{:icon true
|
||||
:type :outline
|
||||
:size 32} :i/reaction]])
|
||||
|
||||
(defn image-button
|
||||
[insets]
|
||||
[quo/button
|
||||
{:on-press (fn []
|
||||
(permissions/request-permissions
|
||||
{:permissions [:read-external-storage :write-external-storage]
|
||||
:on-allowed #(rf/dispatch [:open-modal :photo-selector {:insets insets}])
|
||||
:on-denied (fn []
|
||||
(background-timer/set-timeout
|
||||
#(utils-old/show-popup (i18n/label :t/error)
|
||||
(i18n/label
|
||||
:t/external-storage-denied))
|
||||
50))}))
|
||||
:icon true
|
||||
:type :outline
|
||||
:size 32}
|
||||
:i/image])
|
||||
|
||||
(defn record-audio
|
||||
[record-ref chat-id bottom-inset]
|
||||
[rn/view
|
||||
{:ref record-ref
|
||||
:style (style/record-audio-container bottom-inset)
|
||||
:pointer-events :box-none}
|
||||
[quo/record-audio
|
||||
{:record-audio-permission-granted @input/record-audio-permission-granted
|
||||
:on-init (fn [init-fn]
|
||||
(reset! input/record-audio-reset-fn init-fn)
|
||||
(reset! input/recording-audio?
|
||||
(some? (get @input/reviewing-audio-filepath chat-id)))
|
||||
(when (seq (get @input/input-texts chat-id))
|
||||
(js/setTimeout #(quo.react/set-native-props
|
||||
record-ref
|
||||
#js {:right nil :left -1000}))))
|
||||
:on-start-recording #(reset! input/recording-audio? true)
|
||||
:audio-file (get @input/reviewing-audio-filepath chat-id)
|
||||
:on-reviewing-audio (fn [audio-file]
|
||||
(swap! input/reviewing-audio-filepath assoc
|
||||
chat-id
|
||||
audio-file)
|
||||
(reset! input/reviewing-audio? true))
|
||||
:on-send (fn
|
||||
[{:keys [file-path duration]}]
|
||||
(rf/dispatch [:chat/send-audio file-path duration])
|
||||
(reset! input/recording-audio? false)
|
||||
(reset! input/reviewing-audio? false)
|
||||
(swap! input/reviewing-audio-filepath dissoc chat-id))
|
||||
:on-cancel (fn []
|
||||
(reset! input/recording-audio? false)
|
||||
(reset! input/reviewing-audio? false)
|
||||
(swap! input/reviewing-audio-filepath dissoc chat-id))
|
||||
:on-check-audio-permissions (fn []
|
||||
(permissions/permission-granted?
|
||||
:record-audio
|
||||
#(reset! input/record-audio-permission-granted %)
|
||||
#(reset! input/record-audio-permission-granted false)))
|
||||
:on-request-record-audio-permission (fn []
|
||||
(rf/dispatch
|
||||
[:request-permissions
|
||||
{:permissions [:record-audio]
|
||||
:on-allowed
|
||||
#(reset! input/record-audio-permission-granted true)
|
||||
:on-denied
|
||||
#(js/setTimeout
|
||||
(fn []
|
||||
(alert/show-popup
|
||||
(i18n/label :t/audio-recorder-error)
|
||||
(i18n/label
|
||||
:t/audio-recorder-permissions-error)))
|
||||
50)}]))}]])
|
||||
|
||||
(defn view
|
||||
[send-ref record-ref params bottom-inset chat-id images edit on-send]
|
||||
[rn/view {:style (style/controls bottom-inset)}
|
||||
[composer-images/images-list images]
|
||||
[rn/view {:style style/buttons-container}
|
||||
(when (and (not @input/recording-audio?)
|
||||
(nil? (get @input/reviewing-audio-filepath chat-id)))
|
||||
[:<>
|
||||
[image-button {:bottom bottom-inset}]
|
||||
[rn/view {:width 12}]
|
||||
[reactions-button]
|
||||
[rn/view {:flex 1}]
|
||||
[send-button send-ref params on-send]])]
|
||||
(when (and (not edit) (not (seq images)))
|
||||
[record-audio record-ref chat-id bottom-inset])])
|
|
@ -1,35 +0,0 @@
|
|||
(ns status-im2.contexts.chat.messages.composer.mentions.view
|
||||
(:require [utils.re-frame :as rf]
|
||||
[react-native.core :as rn]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im2.common.contact-list-item.view :as contact-list-item]))
|
||||
|
||||
(defn mention-item
|
||||
[user _ _ text-input-ref]
|
||||
[contact-list-item/contact-list-item
|
||||
{:on-press #(rf/dispatch [:chat.ui/select-mention text-input-ref user])} user])
|
||||
|
||||
(defn f-mentions
|
||||
[{:keys [refs suggestions max-y]} bottom-inset]
|
||||
(let [translate-y (reanimated/use-shared-value 0)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(reanimated/set-shared-value translate-y
|
||||
(reanimated/with-timing (if (seq suggestions) 0 200)))))
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:transform [{:translateY translate-y}]}
|
||||
{:bottom (or bottom-inset 0)
|
||||
:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:z-index 5
|
||||
:elevation 5
|
||||
:max-height (/ max-y 2)})}
|
||||
[rn/flat-list
|
||||
{:keyboard-should-persist-taps :always
|
||||
:data (vals suggestions)
|
||||
:key-fn :key
|
||||
:render-fn mention-item
|
||||
:render-data (:text-input-ref refs)
|
||||
:accessibility-label :mentions-list}]]))
|
|
@ -1,42 +0,0 @@
|
|||
(ns status-im2.contexts.chat.messages.composer.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[status-im.utils.platform :as platform]))
|
||||
|
||||
(defn input-bottom-sheet
|
||||
[window-height]
|
||||
(merge {:border-top-left-radius 20
|
||||
:border-top-right-radius 20
|
||||
:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom (- window-height)
|
||||
:height window-height
|
||||
:flex 1
|
||||
:background-color (colors/theme-colors colors/white colors/neutral-90)
|
||||
:z-index 2}
|
||||
(if platform/ios?
|
||||
{:shadow-radius 16
|
||||
:shadow-opacity 1
|
||||
:shadow-color "rgba(9, 16, 28, 0.04)"
|
||||
:shadow-offset {:width 0 :height -2}}
|
||||
{:elevation 4})))
|
||||
|
||||
(defn bottom-sheet-handle
|
||||
[]
|
||||
{:width 32
|
||||
:height 4
|
||||
:background-color (colors/theme-colors colors/neutral-100 colors/white)
|
||||
:opacity 0.05
|
||||
:border-radius 100
|
||||
:align-self :center
|
||||
:margin-top 8})
|
||||
|
||||
(defn bottom-sheet-background
|
||||
[window-height]
|
||||
{:position :absolute
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0
|
||||
:height window-height
|
||||
:background-color colors/neutral-95-opa-70
|
||||
:z-index 1})
|
|
@ -1,241 +0,0 @@
|
|||
(ns status-im2.contexts.chat.messages.composer.view
|
||||
(:require [clojure.string :as string]
|
||||
[oops.core :refer [oget]]
|
||||
[react-native.core :as rn]
|
||||
[react-native.hooks :as hooks]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[react-native.background-timer :as background-timer]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.messages.composer.style :as style]
|
||||
[status-im2.contexts.chat.messages.composer.controls.view :as controls]
|
||||
[status-im2.contexts.chat.messages.composer.mentions.view :as mentions]
|
||||
[status-im.ui2.screens.chat.composer.edit.view :as edit]
|
||||
[status-im.ui2.screens.chat.composer.input :as input]
|
||||
[status-im.ui2.screens.chat.composer.reply :as reply]
|
||||
[quo.react :refer [set-native-props]]))
|
||||
|
||||
(def initial-content-height (atom nil))
|
||||
(def keyboard-hiding? (atom false))
|
||||
|
||||
(defn minimize
|
||||
[{:keys [min-y set-bg-opacity set-translate-y set-parent-height refs chat-id]}]
|
||||
(set-bg-opacity 0)
|
||||
(set-translate-y (- min-y))
|
||||
(set-parent-height min-y)
|
||||
(when-not (seq (get @input/input-texts chat-id))
|
||||
(set-native-props (:record-ref refs) #js {:right 0 :left 0})))
|
||||
|
||||
(defn maximize
|
||||
[{:keys [max-y set-bg-opacity set-translate-y set-parent-height max-parent-height refs]}]
|
||||
(set-bg-opacity 1)
|
||||
(set-translate-y (- max-y))
|
||||
(set-parent-height max-parent-height)
|
||||
(set-native-props (:record-ref refs) #js {:right nil :left -1000}))
|
||||
|
||||
(defn clean-and-minimize
|
||||
[{:keys [chat-id refs] :as params}]
|
||||
(input/clear-input chat-id refs)
|
||||
(minimize params))
|
||||
|
||||
(def gesture-values (atom {}))
|
||||
|
||||
(defn get-bottom-sheet-gesture
|
||||
[{:keys [translate-y refs keyboard-shown min-y max-y] :as params}]
|
||||
(let [{:keys [text-input-ref]} refs]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/on-start
|
||||
(fn [_]
|
||||
(if (and keyboard-shown (not @input/recording-audio?))
|
||||
(swap! gesture-values assoc :pan-y (reanimated/get-shared-value translate-y))
|
||||
(input/input-focus text-input-ref))))
|
||||
(gesture/on-update
|
||||
(fn [evt]
|
||||
(when (and keyboard-shown (not @input/recording-audio?))
|
||||
(let [tY (oget evt "translationY")]
|
||||
(swap! gesture-values assoc :dy (- tY (:pdy @gesture-values)))
|
||||
(swap! gesture-values assoc :pdy tY)
|
||||
(reanimated/set-shared-value
|
||||
translate-y
|
||||
(max (min (+ tY (:pan-y @gesture-values)) (- min-y)) (- max-y)))))))
|
||||
(gesture/on-end
|
||||
(fn [_]
|
||||
(when (and keyboard-shown (not @input/recording-audio?))
|
||||
(if (< (:dy @gesture-values) 0)
|
||||
(maximize params)
|
||||
(do
|
||||
(reset! keyboard-hiding? true)
|
||||
(minimize params)
|
||||
(background-timer/set-timeout #(rf/dispatch [:dismiss-keyboard]) 200)))))))))
|
||||
|
||||
(defn update-y
|
||||
[{:keys [max-parent-height set-bg-opacity keyboard-shown max-y set-translate-y set-parent-height
|
||||
min-y chat-id]
|
||||
:as params}]
|
||||
(let [content-height (get @input/input-text-content-heights chat-id)
|
||||
new-y (+ min-y
|
||||
(if (and content-height @initial-content-height)
|
||||
(- content-height
|
||||
@initial-content-height)
|
||||
0))]
|
||||
(if (< new-y max-y)
|
||||
(when keyboard-shown
|
||||
(if (> new-y max-parent-height)
|
||||
(set-bg-opacity 1)
|
||||
(set-bg-opacity 0))
|
||||
(set-translate-y (- new-y))
|
||||
(set-parent-height (min new-y max-parent-height)))
|
||||
(if keyboard-shown
|
||||
(maximize params)
|
||||
(set-bg-opacity 0)))))
|
||||
|
||||
(defn get-input-content-change
|
||||
[{:keys [chat-id] :as params}]
|
||||
(fn [evt]
|
||||
(let [new-height (oget evt "nativeEvent.contentSize.height")]
|
||||
(swap! input/input-text-content-heights assoc chat-id new-height)
|
||||
(if @initial-content-height
|
||||
(do
|
||||
;;on Android when last symbol removed, height value is wrong, like 300px
|
||||
(when (string/blank? (get @input/input-texts chat-id))
|
||||
(swap! input/input-text-content-heights assoc chat-id @initial-content-height))
|
||||
(update-y params))
|
||||
(reset! initial-content-height new-height)))))
|
||||
|
||||
(defn effect!
|
||||
[{:keys [keyboard-shown reply edit suggestions images] :as params}]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(when (or (not @keyboard-hiding?)
|
||||
(and @keyboard-hiding? (not keyboard-shown)))
|
||||
(reset! keyboard-hiding? false)
|
||||
(if (not keyboard-shown)
|
||||
(minimize params)
|
||||
(update-y params))))
|
||||
[reply edit suggestions images]))
|
||||
|
||||
(defn prepare-params
|
||||
[[refs window-height translate-y bg-opacity bg-bottom min-y max-y parent-height
|
||||
max-parent-height chat-id suggestions reply edit images keyboard-shown]]
|
||||
{:chat-id chat-id
|
||||
:translate-y translate-y
|
||||
:min-y min-y
|
||||
:max-y max-y
|
||||
:max-parent-height max-parent-height
|
||||
:parent-height parent-height
|
||||
:suggestions suggestions
|
||||
:reply reply
|
||||
:edit edit
|
||||
:images images
|
||||
:refs refs
|
||||
:keyboard-shown keyboard-shown
|
||||
:bg-opacity bg-opacity
|
||||
:bg-bottom bg-bottom
|
||||
:window-height window-height
|
||||
:set-bg-opacity (fn [value]
|
||||
(reanimated/set-shared-value bg-bottom (if (= value 1) 0 (- window-height)))
|
||||
(reanimated/set-shared-value bg-opacity (reanimated/with-timing value)))
|
||||
:set-translate-y (fn [value]
|
||||
(reanimated/set-shared-value translate-y (reanimated/with-timing value)))
|
||||
:set-parent-height (fn [value]
|
||||
(reanimated/set-shared-value parent-height (reanimated/with-timing value)))})
|
||||
|
||||
;; IMPORTANT! PLEASE READ BEFORE MAKING CHANGES
|
||||
|
||||
;; 1) insets are not stable so we can't rely on the first values and we have to use it from render
|
||||
;; function, so effect will be triggered a few times, for example first time it will be 0 and second
|
||||
;; time 38 on ios, and 400 and 0 on Android
|
||||
;; 2) for keyboard we use hooks, for some reason it triggers effect a few times with the same values,
|
||||
;; also it changes value only when keyboard completely closed or opened, that's why its not visually
|
||||
;; smooth, there is a small visible delay between keyboard and sheet animations
|
||||
;; 3) when we minimize sheet and hide keyboard in `sheet-gesture` function, keyboard hook triggers
|
||||
;; effect and there is a visual glitch when we have more than one line of text, so we use
|
||||
;; `keyboard-hiding?` flag
|
||||
;; 4) we store input content height in `input/input-text-content-heights` atom , we need it when chat
|
||||
;; screen is reopened
|
||||
(defn f-composer
|
||||
[_ _]
|
||||
(let [text-input-ref (rn/create-ref)
|
||||
send-ref (rn/create-ref)
|
||||
record-ref (rn/create-ref)
|
||||
refs {:send-ref send-ref
|
||||
:text-input-ref text-input-ref
|
||||
:record-ref record-ref}]
|
||||
(fn [chat-id insets]
|
||||
(let [reply (rf/sub [:chats/reply-message])
|
||||
edit (rf/sub [:chats/edit-message])
|
||||
suggestions (rf/sub [:chat/mention-suggestions])
|
||||
images (rf/sub [:chats/sending-image])
|
||||
|
||||
bottom-inset (max 20 (:bottom insets))
|
||||
{window-height :height} (rn/get-window)
|
||||
{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard)
|
||||
translate-y (reanimated/use-shared-value 0)
|
||||
bg-opacity (reanimated/use-shared-value 0)
|
||||
bg-bottom (reanimated/use-shared-value (- window-height))
|
||||
|
||||
suggestions? (and (seq suggestions)
|
||||
keyboard-shown
|
||||
(not @keyboard-hiding?))
|
||||
|
||||
max-y (- window-height
|
||||
(- (if (> keyboard-height 0)
|
||||
keyboard-height
|
||||
360)
|
||||
bottom-inset)
|
||||
46)
|
||||
|
||||
min-y (+ 108
|
||||
bottom-inset
|
||||
(if suggestions?
|
||||
(min (/ max-y 2)
|
||||
(+ 16
|
||||
(* 46 (dec (count suggestions)))))
|
||||
(+ 0
|
||||
(when (and
|
||||
(or edit reply)
|
||||
(not @input/recording-audio?))
|
||||
38)
|
||||
(when (seq images) 80))))
|
||||
|
||||
parent-height (reanimated/use-shared-value min-y)
|
||||
max-parent-height (Math/abs (- max-y 110 bottom-inset))
|
||||
|
||||
params
|
||||
(prepare-params
|
||||
[refs window-height translate-y bg-opacity bg-bottom min-y max-y parent-height
|
||||
max-parent-height chat-id suggestions reply edit images keyboard-shown])
|
||||
|
||||
input-content-change (get-input-content-change params)
|
||||
bottom-sheet-gesture (get-bottom-sheet-gesture params)]
|
||||
(effect! params)
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:height parent-height}
|
||||
{})}
|
||||
;;;;input
|
||||
[gesture/gesture-detector {:gesture bottom-sheet-gesture}
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:transform [{:translateY translate-y}]}
|
||||
(style/input-bottom-sheet window-height))}
|
||||
[rn/view {:style (style/bottom-sheet-handle)}]
|
||||
[edit/edit-message-auto-focus-wrapper text-input-ref edit #(clean-and-minimize params)]
|
||||
[reply/reply-message-auto-focus-wrapper text-input-ref reply]
|
||||
[rn/view
|
||||
{:style {:height (- max-y (- min-y 38))}}
|
||||
[input/text-input
|
||||
{:chat-id chat-id
|
||||
:on-content-size-change input-content-change
|
||||
:sending-image (seq images)
|
||||
:refs refs}]]]]
|
||||
(if suggestions?
|
||||
[:f> mentions/f-mentions (select-keys params [:refs :suggestions :max-y]) bottom-inset]
|
||||
[controls/view send-ref record-ref params bottom-inset chat-id images
|
||||
edit #(clean-and-minimize params)])
|
||||
;;;;black background
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:opacity bg-opacity
|
||||
:transform [{:translateY bg-bottom}]}
|
||||
(style/bottom-sheet-background window-height))}]]))))
|
|
@ -1,5 +1,6 @@
|
|||
(ns status-im2.contexts.chat.messages.content.pin.view
|
||||
(:require [react-native.core :as rn]
|
||||
[status-im2.contexts.chat.composer.reply.view :as reply]
|
||||
[status-im2.contexts.chat.messages.content.pin.style :as style]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
|
@ -53,4 +54,4 @@
|
|||
(old-style/message-timestamp-text))
|
||||
:accessibility-label :message-timestamp}
|
||||
timestamp-str]]
|
||||
[old-message/quoted-message quoted-message true]]])
|
||||
[reply/quoted-message quoted-message false true]]])
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
(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]
|
||||
[status-im2.contexts.chat.messages.content.unknown.view :as content.unknown]
|
||||
[status-im2.contexts.chat.messages.content.text.view :as content.text]
|
||||
[status-im2.contexts.chat.messages.drawers.view :as drawers]
|
||||
[status-im2.contexts.chat.messages.content.reactions.view :as reactions]
|
||||
[status-im2.contexts.chat.messages.content.status.view :as status]
|
||||
[status-im2.contexts.chat.messages.content.system.text.view :as system.text]
|
||||
[status-im2.contexts.chat.messages.content.album.view :as album]
|
||||
[status-im2.contexts.chat.messages.avatar.view :as avatar]
|
||||
[status-im2.contexts.chat.messages.content.image.view :as image]
|
||||
[status-im2.contexts.chat.messages.content.audio.view :as audio]
|
||||
[quo2.core :as quo]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im.ui2.screens.chat.messages.message :as old-message]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
[utils.datetime :as datetime]
|
||||
[reagent.core :as reagent]
|
||||
[utils.address :as address]))
|
||||
(: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]
|
||||
[status-im2.contexts.chat.messages.content.unknown.view :as content.unknown]
|
||||
[status-im2.contexts.chat.messages.content.text.view :as content.text]
|
||||
[status-im2.contexts.chat.messages.drawers.view :as drawers]
|
||||
[status-im2.contexts.chat.messages.content.reactions.view :as reactions]
|
||||
[status-im2.contexts.chat.messages.content.status.view :as status]
|
||||
[status-im2.contexts.chat.messages.content.system.text.view :as system.text]
|
||||
[status-im2.contexts.chat.messages.content.album.view :as album]
|
||||
[status-im2.contexts.chat.messages.avatar.view :as avatar]
|
||||
[status-im2.contexts.chat.messages.content.image.view :as image]
|
||||
[status-im2.contexts.chat.messages.content.audio.view :as audio]
|
||||
[quo2.core :as quo]
|
||||
[utils.re-frame :as rf]
|
||||
[status-im.ui2.screens.chat.messages.message :as old-message]
|
||||
[status-im2.contexts.chat.composer.reply.view :as reply]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
[utils.datetime :as datetime]
|
||||
[reagent.core :as reagent]
|
||||
[utils.address :as address]))
|
||||
|
||||
(def delivery-state-showing-time-ms 3000)
|
||||
|
||||
|
@ -109,7 +111,7 @@
|
|||
:on-long-press #(on-long-press message-data context)}
|
||||
[rn/view {:style {:padding-vertical 8}}
|
||||
(when (and (seq response-to) quoted-message)
|
||||
[old-message/quoted-message quoted-message])
|
||||
[reply/quoted-message quoted-message])
|
||||
[rn/view
|
||||
{:style {:padding-horizontal 12
|
||||
:flex-direction :row}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
(:require [quo2.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.ui2.screens.chat.components.reply.view :as components.reply]
|
||||
[status-im2.contexts.chat.composer.reply.view :as reply]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
[status-im2.constants :as constants]
|
||||
[utils.i18n :as i18n]
|
||||
|
@ -47,7 +47,7 @@
|
|||
(not= content-type constants/content-type-audio))
|
||||
[{:type :main
|
||||
:on-press #(react/copy-to-clipboard
|
||||
(components.reply/get-quoted-text-with-mentions
|
||||
(reply/get-quoted-text-with-mentions
|
||||
(get content :parsed-text)))
|
||||
:label (i18n/label :t/copy-text)
|
||||
:icon :i/copy
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
(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]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[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.bottom-sheet-composer.utils :as utils]
|
||||
[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 calc-shell-position
|
||||
[y]
|
||||
(let [{:keys [input-content-height focused?]} (rf/sub [:chats/current-chat-input])
|
||||
reply (rf/sub [:chats/reply-message])
|
||||
edit (rf/sub [:chats/edit-message])
|
||||
lines (utils/calc-lines input-content-height)
|
||||
base (if (or reply edit)
|
||||
(- composer.constants/edit-container-height)
|
||||
0)]
|
||||
(if (not focused?)
|
||||
(if (> lines 1) (+ -18 base) base)
|
||||
(if (> lines 12)
|
||||
(reanimated/get-shared-value y)
|
||||
(if (> lines 1) (- (- input-content-height composer.constants/input-height base)) base)))))
|
||||
|
||||
(defn shell-button
|
||||
[insets]
|
||||
(let [y (reanimated/use-shared-value 0)
|
||||
shell-position (calc-shell-position y)]
|
||||
(rn/use-effect (fn []
|
||||
(reanimated/animate y shell-position))
|
||||
[shell-position])
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:transform [{:translate-y y}]}
|
||||
{:bottom (+ composer.constants/composer-default-height (:bottom insets) 6)
|
||||
:position :absolute
|
||||
:left 0
|
||||
:right 0})}
|
||||
[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)
|
||||
:style {:align-self :center}}}
|
||||
(when @show-floating-scroll-down-button
|
||||
{:scroll-to-bottom {:on-press scroll-to-bottom}}))
|
||||
{}]]))
|
||||
|
||||
|
||||
(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}]
|
||||
[:f> shell-button insets]])))
|
||||
|
||||
|
||||
(defn use-keyboard-visibility
|
||||
[]
|
||||
(let [show-listener (atom nil)
|
||||
hide-listener (atom nil)
|
||||
shown? (atom nil)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(reset! show-listener
|
||||
(.addListener rn/keyboard "keyboardWillShow" #(reset! shown? true)))
|
||||
(reset! hide-listener
|
||||
(.addListener rn/keyboard "keyboardWillHide" #(reset! shown? false)))
|
||||
(fn []
|
||||
(.remove ^js @show-listener)
|
||||
(.remove ^js @hide-listener))))
|
||||
{:shown? shown?}))
|
||||
|
||||
(defn- f-messages-list
|
||||
[chat insets]
|
||||
(let [{keyboard-shown? :shown?} (use-keyboard-visibility)]
|
||||
[messages-list-content chat insets keyboard-shown?]))
|
||||
|
||||
(defn messages-list
|
||||
[chat insets]
|
||||
[:f> f-messages-list chat insets])
|
|
@ -4,16 +4,19 @@
|
|||
[react-native.background-timer :as background-timer]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[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.composer.utils :as utils]
|
||||
[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]))
|
||||
[utils.re-frame :as rf]
|
||||
[status-im2.contexts.chat.composer.constants :as composer.constants]))
|
||||
|
||||
(defonce messages-list-ref (atom nil))
|
||||
|
||||
|
@ -90,7 +93,7 @@
|
|||
|
||||
(defn render-fn
|
||||
[{:keys [type value deleted? deleted-for-me? content-type] :as message-data} _ _
|
||||
{:keys [keyboard-shown context]}]
|
||||
{:keys [context keyboard-shown]}]
|
||||
[rn/view {:style (when platform/android? {:scaleY -1})}
|
||||
(if (= type :datemark)
|
||||
[quo/divider-date value]
|
||||
|
@ -102,47 +105,114 @@
|
|||
[content.deleted/deleted-message message-data context]
|
||||
[message/message-with-reactions message-data context keyboard-shown])]))])
|
||||
|
||||
(defn messages-list
|
||||
[{:keys [chat-id] :as chat}]
|
||||
(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
|
||||
[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 (+ bottom-space 32)
|
||||
:padding-bottom 16}
|
||||
:scroll-indicator-insets {:top bottom-space} ; iOS only
|
||||
:keyboard-dismiss-mode :interactive
|
||||
:keyboard-should-persist-taps :handled
|
||||
:onMomentumScrollBegin state/start-scrolling
|
||||
:onMomentumScrollEnd state/stop-scrolling
|
||||
:scrollEventThrottle 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}]
|
||||
(defn calc-shell-position
|
||||
[curr-pos input-content-height focused? reply edit images]
|
||||
(let [lines (utils/calc-lines input-content-height)
|
||||
base (if reply (- composer.constants/reply-container-height) 0)
|
||||
base (if edit (- composer.constants/edit-container-height) base)
|
||||
base (if (seq images) (- composer.constants/images-container-height) base)]
|
||||
(if (not focused?)
|
||||
(if (> lines 1) (+ (- composer.constants/multiline-minimized-height) base) base)
|
||||
(if (> lines 12)
|
||||
curr-pos
|
||||
(if (> lines 1) (- (- input-content-height composer.constants/input-height base)) base)))))
|
||||
|
||||
(def memoized-calc-shell-position (memoize calc-shell-position))
|
||||
|
||||
(defn shell-button
|
||||
[insets]
|
||||
(let [y (reanimated/use-shared-value 0)
|
||||
{:keys [input-content-height focused?]} (rf/sub [:chats/current-chat-input])
|
||||
reply (rf/sub [:chats/reply-message])
|
||||
edit (rf/sub [:chats/edit-message])
|
||||
images (rf/sub [:chats/sending-image])
|
||||
curr-pos (reanimated/get-shared-value y)
|
||||
shell-position
|
||||
(memoized-calc-shell-position curr-pos input-content-height focused? reply edit images)]
|
||||
(rn/use-effect (fn []
|
||||
(reanimated/animate y shell-position))
|
||||
[shell-position])
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style
|
||||
{:transform [{:translate-y y}]}
|
||||
{:bottom (+ composer.constants/composer-default-height (:bottom insets) 6)
|
||||
:position :absolute
|
||||
:left 0
|
||||
:right 0})}
|
||||
[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)}}
|
||||
{:on-press (fn []
|
||||
(rf/dispatch [:chat/close true])
|
||||
(rf/dispatch [:shell/navigate-to-jump-to]))
|
||||
:label (i18n/label :t/jump-to)
|
||||
:style {:align-self :center}}}
|
||||
(when @show-floating-scroll-down-button
|
||||
{:scroll-to-bottom {:on-press scroll-to-bottom}}))
|
||||
{:position :absolute
|
||||
:bottom 6}]]))
|
||||
{}]]))
|
||||
|
||||
(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}]
|
||||
[:f> shell-button insets]])))
|
||||
|
||||
;; This should be replaced with keyboard hook. It has to do with flat-list probably. The keyboard-shown
|
||||
;; value
|
||||
;; updates in the parent component, but does not get passed to the children.
|
||||
;; When using listeners and resetting the value on an atom it works.
|
||||
(defn use-keyboard-visibility
|
||||
[]
|
||||
(let [show-listener (atom nil)
|
||||
hide-listener (atom nil)
|
||||
shown? (atom nil)]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(reset! show-listener
|
||||
(.addListener rn/keyboard "keyboardWillShow" #(reset! shown? true)))
|
||||
(reset! hide-listener
|
||||
(.addListener rn/keyboard "keyboardWillHide" #(reset! shown? false)))
|
||||
(fn []
|
||||
(.remove ^js @show-listener)
|
||||
(.remove ^js @hide-listener))))
|
||||
{:shown? shown?}))
|
||||
|
||||
(defn- f-messages-list
|
||||
[chat insets]
|
||||
(let [{keyboard-shown? :shown?} (use-keyboard-visibility)]
|
||||
[messages-list-content chat insets keyboard-shown?]))
|
||||
|
||||
(defn messages-list
|
||||
[chat insets]
|
||||
[:f> f-messages-list chat insets])
|
||||
|
|
|
@ -4,14 +4,11 @@
|
|||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.config :as config]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.chat.bottom-sheet-composer.view :as bottom-sheet-composer]
|
||||
[status-im2.contexts.chat.messages.composer.view :as composer]
|
||||
[status-im2.contexts.chat.composer.view :as composer]
|
||||
[status-im2.contexts.chat.messages.contact-requests.bottom-drawer :as
|
||||
contact-requests.bottom-drawer]
|
||||
[status-im2.contexts.chat.messages.list.view :as messages.list]
|
||||
[status-im2.contexts.chat.messages.list.new-temp-view :as messages.list.new]
|
||||
[status-im2.contexts.chat.messages.pin.banner.view :as pin.banner]
|
||||
[status-im2.navigation.state :as navigation.state]
|
||||
[utils.debounce :as debounce]
|
||||
|
@ -78,14 +75,10 @@
|
|||
:keyboardVerticalOffset (- (:bottom insets))}
|
||||
[page-nav]
|
||||
[pin.banner/banner chat-id]
|
||||
(if config/new-composer-enabled?
|
||||
[messages.list.new/messages-list chat insets]
|
||||
[messages.list/messages-list chat insets])
|
||||
[messages.list/messages-list chat insets]
|
||||
(if-not able-to-send-message?
|
||||
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]
|
||||
(if config/new-composer-enabled?
|
||||
[bottom-sheet-composer/bottom-sheet-composer insets]
|
||||
[:f> composer/f-composer chat-id insets]))]))
|
||||
[:f> composer/composer insets])]))
|
||||
|
||||
|
||||
(defn chat
|
||||
|
|
Loading…
Reference in New Issue