Compare commits

...
Author SHA1 Message Date
Omar Basem b00f0971bc velocity gesture test 2023-05-09 14:47:03 +04:00
Omar Basem a6fdc9b118 fix shadow 2023-05-09 13:11:37 +04:00
Omar Basem 4d1ff17ba1 qa review 2023-05-09 12:37:58 +04:00
Omar Basem 8a02803d9f updates 2023-05-09 12:37:58 +04:00
Omar Basem f9ee7e0559 review 2023-05-09 12:37:58 +04:00
Omar Basem 978540d5b7 lint 2023-05-09 12:37:58 +04:00
Omar Basem 3c02518d4a fix 2023-05-09 12:37:57 +04:00
Omar Basem 7bafb3d82b formatting 2023-05-09 12:37:57 +04:00
Omar Basem 59d1383177 lint 2023-05-09 12:37:57 +04:00
Omar Basem 5fb48949ae lint 2023-05-09 12:37:56 +04:00
Omar Basem 78e9102264 review 2023-05-09 12:37:56 +04:00
Omar Basem 2b2170dfff review 2023-05-09 12:37:56 +04:00
Omar Basem ab94f9b017 qa review 2023-05-09 12:37:56 +04:00
Omar Basem 1f5047eea7 review 2023-05-09 12:37:55 +04:00
Omar Basem e6e77e4a58 fix test 2023-05-09 12:37:55 +04:00
Omar Basem 22d9c14ae6 lint 2023-05-09 12:37:55 +04:00
Omar Basem 84bbfe8f9d feat: new composer
review

lint

review

udpates

updates

updates

udpates

updates

updates

lint

updates

composer shell button

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

lint

review

review

review

review

revert podfile.lock

selectable-input

updates

updates

updates

updates
2023-05-09 12:37:55 +04:00
54 changed files with 811 additions and 1951 deletions
+5 -2
View File
@@ -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
View File
@@ -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))))
+6 -22
View File
@@ -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]
-2
View File
@@ -158,5 +158,3 @@
["enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@test.waku.nodes.status.im"]})
(def default-kdf-iterations 3200)
(def ^:const new-composer-enabled? false)
-1
View File
@@ -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,4 +1,4 @@
(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]))
@@ -29,8 +29,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,43 @@
(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)))
(reset! sending-images? false)))
(defn edit-effect
[{:keys [text-value saved-cursor-position]}
{:keys [editing? input-ref]}
[{:keys [input-ref]}
{:keys [text-value saved-cursor-position]}
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))))
(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-effect
[{: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 audio-effect
[{:keys [recording? gesture-enabled?]}
@@ -105,7 +116,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 +124,51 @@
(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-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)))
@@ -54,7 +56,7 @@
(defn drag-gesture
[{:keys [input-ref] :as props}
{:keys [gesture-enabled?] :as state}
{:keys [gesture-enabled? text-value] :as state}
{:keys [height saved-height last-height opacity background-y container-opacity] :as animations}
{:keys [max-height lines] :as dimensions}
keyboard-shown]
@@ -65,6 +67,7 @@
(gesture/on-start (fn [event]
(if-not keyboard-shown
(do ; focus and end
(reset! text-value (str (oops/oget event "velocityY")))
(when (< (oops/oget event "velocityY") constants/velocity-threshold)
(reanimated/set-shared-value container-opacity 1)
(reanimated/set-shared-value last-height max-height))
@@ -74,21 +77,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 +106,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,4 +1,4 @@
(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]
@@ -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,9 +116,24 @@
(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
@@ -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
@@ -83,24 +86,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 +130,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-effect props state edit)
(effects/reply-effect 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) (+ -18 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