diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index 2aa5549e44..695caa3317 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -107,14 +107,15 @@ [{:keys [db] :as cofx} message] (let [current-chat-id (:current-chat-id db) text (get-in message [:content :text])] - {:db (-> db - (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] - message) - (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil) - (update-in [:chat/inputs current-chat-id :metadata] - dissoc - :sending-image)) - :dispatch [:mention/to-input-field text current-chat-id]})) + {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] + message) + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil) + (update-in [:chat/inputs current-chat-id :metadata] + dissoc + :sending-image)) + :dispatch-n [[:chat.ui/set-chat-input-text nil current-chat-id] + [:mention/to-input-field text current-chat-id]]})) (rf/defn show-contact-request-input "Sets reference to previous chat message and focuses on input" diff --git a/src/status_im2/contexts/chat/composer/edit/view.cljs b/src/status_im2/contexts/chat/composer/edit/view.cljs index 9cfe5f4d5c..526b3303e4 100644 --- a/src/status_im2/contexts/chat/composer/edit/view.cljs +++ b/src/status_im2/contexts/chat/composer/edit/view.cljs @@ -11,7 +11,7 @@ [utils.re-frame :as rf])) (defn edit-message - [state] + [{:keys [text-value input-ref]}] [rn/view {:style style/container :accessibility-label :edit-message} @@ -30,20 +30,18 @@ {:size 24 :icon-only? true :accessibility-label :edit-cancel-button - :on-press (fn [] - (utils/cancel-edit-message state) - (rf/dispatch [:chat.ui/cancel-message-edit])) + :on-press #(utils/cancel-edit-message text-value input-ref) :type :outline} :i/close]]) (defn- f-view - [state] + [props] (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 state])])) + (when edit [edit-message props])])) (defn view - [state] - [:f> f-view state]) + [props] + [:f> f-view props]) diff --git a/src/status_im2/contexts/chat/composer/effects.cljs b/src/status_im2/contexts/chat/composer/effects.cljs index 9ff63c82a7..326a05170c 100644 --- a/src/status_im2/contexts/chat/composer/effects.cljs +++ b/src/status_im2/contexts/chat/composer/effects.cljs @@ -6,6 +6,7 @@ [react-native.core :as rn] [react-native.platform :as platform] [react-native.reanimated :as reanimated] + [reagent.core :as reagent] [status-im2.contexts.chat.composer.constants :as constants] [status-im2.contexts.chat.composer.keyboard :as kb] [status-im2.contexts.chat.composer.utils :as utils] @@ -105,53 +106,62 @@ (defn use-edit [{:keys [input-ref]} - {:keys [text-value saved-cursor-position]} - {:keys [edit]}] - (rn/use-effect - (fn [] - (let [edit-text (get-in edit [:content :text]) - text-value-count (count @text-value)] - (when (and edit @input-ref) - ;; A small setTimeout is necessary to ensure the statement is enqueued and will get executed - ;; ASAP. - ;; https://github.com/software-mansion/react-native-screens/issues/472 - (js/setTimeout #(.focus ^js @input-ref) 250) - (.setNativeProps ^js @input-ref (clj->js {:text edit-text})) - (reset! text-value edit-text) - (reset! saved-cursor-position (if (zero? text-value-count) - (count edit-text) - text-value-count))))) - [(:message-id edit)])) + {:keys [text-value saved-cursor-position cursor-position]} + {:keys [edit input-with-mentions]} + messages-list-on-layout-finished?] + (let [mention? (some #(= :mention (first %)) (seq input-with-mentions)) + composer-just-opened? (not @messages-list-on-layout-finished?)] + (rn/use-effect + (fn [] + (let [mention-text (reduce (fn [acc item] + (str acc (second item))) + "" + input-with-mentions) + edit-text (cond + mention? mention-text + ;; NOTE: using text-value for cases when the user + ;; leaves the app with an unfinished edit and re-opens + ;; the chat. + (and (seq @text-value) composer-just-opened?) + @text-value + :else (get-in edit [:content :text])) + selection-pos (count edit-text) + inject-edit-text (fn [] + (reset! text-value edit-text) + (reset! cursor-position selection-pos) + (reset! saved-cursor-position selection-pos) + (when @input-ref + (.setNativeProps ^js @input-ref + (clj->js {:text edit-text}))))] + + (when (and edit @input-ref) + ;; NOTE: A small setTimeout is necessary to ensure the focus is enqueued and is executed + ;; ASAP. Check https://github.com/software-mansion/react-native-screens/issues/472 + ;; + ;; The nested setTimeout is necessary to avoid both `on-focus` and + ;; `on-content-size-change` handlers triggering the height animation simultaneously, as + ;; this causes a jump in the + ;; UI. This way, `on-focus` will trigger first without changing the height, after which + ;; `on-content-size-change` will animate the height of the input based on the injected + ;; text. + (js/setTimeout #(do (when @messages-list-on-layout-finished? (.focus ^js @input-ref)) + (reagent/next-tick inject-edit-text)) + 600)))) + [(:message-id edit)]))) (defn use-reply [{:keys [input-ref]} {:keys [container-opacity]} - {:keys [reply]}] + {:keys [reply]} + messages-list-on-layout-finished?] (rn/use-effect (fn [] (when reply (reanimated/animate container-opacity 1)) - (when (and reply @input-ref) - (js/setTimeout #(.focus ^js @input-ref) 250))) + (when (and reply @input-ref @messages-list-on-layout-finished?) + (js/setTimeout #(.focus ^js @input-ref) 600))) [(:message-id reply)])) -(defn edit-mentions - [{:keys [input-ref]} {:keys [text-value cursor-position]} {:keys [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]} diff --git a/src/status_im2/contexts/chat/composer/gesture.cljs b/src/status_im2/contexts/chat/composer/gesture.cljs index 3d786ad845..bc737963a4 100644 --- a/src/status_im2/contexts/chat/composer/gesture.cljs +++ b/src/status_im2/contexts/chat/composer/gesture.cljs @@ -43,8 +43,7 @@ (reset! emoji-kb-extra-height nil)) (reset! maximized? false) (rf/dispatch [:chat.ui/set-input-maximized false]) - (when @input-ref - (.blur ^js @input-ref))) + (utils/blur-input input-ref)) (defn bounce-back [{:keys [height saved-height opacity background-y]} @@ -96,13 +95,13 @@ max-height bounded-height saved-height)) - (when @input-ref ; sheet at min-height, collapse keyboard - (.blur ^js @input-ref))))))) + ; sheet at min-height, collapse keyboard + (utils/blur-input input-ref)))))) (gesture/on-end (fn [] (let [diff (- (reanimated/get-shared-value height) (reanimated/get-shared-value saved-height))] (if @gesture-enabled? - (if (>= diff 0) + (if (and @expanding? (>= diff 0)) (if (> diff constants/drag-threshold) (maximize state animations dimensions) (bounce-back animations dimensions starting-opacity)) diff --git a/src/status_im2/contexts/chat/composer/handlers.cljs b/src/status_im2/contexts/chat/composer/handlers.cljs index 361897695f..1231df45cd 100644 --- a/src/status_im2/contexts/chat/composer/handlers.cljs +++ b/src/status_im2/contexts/chat/composer/handlers.cljs @@ -23,12 +23,14 @@ show-floating-scroll-down-button?] (reset! focused? true) (rf/dispatch [:chat.ui/set-input-focused true]) - (reanimated/animate height (reanimated/get-shared-value last-height)) - (reanimated/set-shared-value saved-height (reanimated/get-shared-value last-height)) - (reanimated/animate container-opacity 1) - (when (> (reanimated/get-shared-value last-height) (* constants/background-threshold max-height)) - (reanimated/animate opacity 1) - (reanimated/set-shared-value background-y 0)) + (let [last-height-value (reanimated/get-shared-value last-height)] + (reanimated/animate height last-height-value) + (reanimated/set-shared-value saved-height last-height-value) + (reanimated/animate container-opacity 1) + (when (> last-height-value (* constants/background-threshold max-height)) + (reanimated/animate opacity 1) + (reanimated/set-shared-value background-y 0))) + (js/setTimeout #(reset! lock-selection? false) 300) (when (and (not-empty @text-value) @input-ref) (.setNativeProps ^js @input-ref @@ -72,7 +74,7 @@ "Save new text height, expand composer if possible, show background overlay if needed" [event {:keys [maximized? lock-layout? text-value]} - {:keys [height saved-height opacity background-y]} + {:keys [height saved-height last-height opacity background-y]} {:keys [content-height window-height max-height]} keyboard-shown] (when keyboard-shown @@ -87,8 +89,9 @@ max-height) new-height (min new-height max-height)] (reset! content-height content-size) - (when (utils/update-height? content-size height max-height maximized?) + (when (utils/update-height? content-size height max-height) (reanimated/animate height new-height) + (reanimated/set-shared-value last-height new-height) (reanimated/set-shared-value saved-height new-height)) (when (= new-height max-height) (reset! maximized? true) diff --git a/src/status_im2/contexts/chat/composer/keyboard.cljs b/src/status_im2/contexts/chat/composer/keyboard.cljs index 7446bd4c59..352e6b6061 100644 --- a/src/status_im2/contexts/chat/composer/keyboard.cljs +++ b/src/status_im2/contexts/chat/composer/keyboard.cljs @@ -4,7 +4,8 @@ [react-native.async-storage :as async-storage] [react-native.core :as rn] [react-native.platform :as platform] - [react-native.reanimated :as reanimated])) + [react-native.reanimated :as reanimated] + [status-im2.contexts.chat.composer.utils :as utils])) (defn get-kb-height [curr-height default-height] @@ -20,18 +21,24 @@ (async-storage/set-item! :kb-default-height (str height))))) (defn handle-emoji-kb-ios - "Opening emoji KB on iOS while maximized will cause a flicker up and down. This method handles that." + "Opening emoji KB on iOS will cause a flicker up and down due to height differences. + This method handles that by adding the extra difference between the keyboards. When the input is + expanded to a point where the added difference will make the composer go beyond the screen causing a flicker, + we're subtracting the difference so it only reaches the allowed max-height. We're not animating these + changes to make it appear seamless during transitions between keyboard types when maximized." [event {:keys [emoji-kb-extra-height]} - {:keys [text-value]} + {:keys [text-value kb-height]} {:keys [height saved-height]} {:keys [max-height]}] - (let [start-h (oops/oget event "startCoordinates.height") - end-h (oops/oget event "endCoordinates.height") - diff (- end-h start-h) - max-height-diff (- max-height diff) - curr-text @text-value] - (if (> (reanimated/get-shared-value height) max-height-diff) + (let [start-h (oops/oget event "startCoordinates.height") + end-h (oops/oget event "endCoordinates.height") + diff (- end-h start-h) + max-height-diff (- max-height diff) + curr-text @text-value + bigger-than-default-kb? (> end-h @kb-height) + almost-expanded? (> (reanimated/get-shared-value height) max-height-diff)] + (if (and almost-expanded? bigger-than-default-kb? (pos? diff)) (do (reanimated/set-shared-value height (- (reanimated/get-shared-value height) diff)) (reanimated/set-shared-value saved-height (- (reanimated/get-shared-value saved-height) diff)) @@ -58,8 +65,8 @@ #(handle-emoji-kb-ios % props state animations dimensions))) (reset! keyboard-hide-listener (.addListener rn/keyboard "keyboardDidHide" - #(when (and platform/android? @input-ref) - (.blur ^js @input-ref))))) + #(when platform/android? + (utils/blur-input input-ref))))) (defn handle-refocus-emoji-kb-ios [{:keys [saved-emoji-kb-extra-height]} diff --git a/src/status_im2/contexts/chat/composer/reply/view.cljs b/src/status_im2/contexts/chat/composer/reply/view.cljs index e43717a52b..89c32e2a61 100644 --- a/src/status_im2/contexts/chat/composer/reply/view.cljs +++ b/src/status_im2/contexts/chat/composer/reply/view.cljs @@ -9,6 +9,7 @@ [status-im2.constants :as constant] [status-im2.contexts.chat.composer.constants :as constants] [status-im2.contexts.chat.composer.reply.style :as style] + [status-im2.contexts.chat.composer.utils :as utils] [utils.ens.stateofus :as stateofus] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -85,7 +86,7 @@ (defn quoted-message [{:keys [from content-type contentType parsed-text content deleted? deleted-for-me? album-images-count]} - in-chat-input? pin? recording-audio?] + in-chat-input? pin? recording-audio? input-ref] (let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from]) current-public-key (rf/sub [:multiaccount/public-key]) content-type (or content-type contentType) @@ -136,7 +137,7 @@ {:icon-only? true :size 24 :accessibility-label :reply-cancel-button - :on-press #(rf/dispatch [:chat.ui/cancel-message-reply]) + :on-press #(utils/cancel-reply-message input-ref) :type :outline} :i/close]) (when (and in-chat-input? recording-audio?) @@ -148,13 +149,13 @@ :style style/gradient}])])) (defn- f-view - [recording?] + [recording? input-ref] (let [reply (rf/sub [:chats/reply-message]) 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 [quoted-message reply true false recording?])])) + (when reply [quoted-message reply true false recording? input-ref])])) (defn view - [{:keys [recording?]}] - [:f> f-view @recording?]) + [{:keys [recording?]} input-ref] + [:f> f-view @recording? input-ref]) diff --git a/src/status_im2/contexts/chat/composer/utils.cljs b/src/status_im2/contexts/chat/composer/utils.cljs index 6e5090cd79..f8c7e892c5 100644 --- a/src/status_im2/contexts/chat/composer/utils.cljs +++ b/src/status_im2/contexts/chat/composer/utils.cljs @@ -16,11 +16,10 @@ (max min-v (min v max-v))) (defn update-height? - [content-size height max-height maximized?] - (when-not @maximized? - (let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))] - (and (not= (reanimated/get-shared-value height) max-height) - (> diff constants/content-change-threshold))))) + [content-size height max-height] + (let [diff (Math/abs (- content-size (reanimated/get-shared-value height)))] + (and (not= (reanimated/get-shared-value height) max-height) + (> diff constants/content-change-threshold)))) (defn show-top-gradient? [y lines max-lines gradient-opacity focused?] @@ -100,10 +99,28 @@ (not reply?) (not audio?))) +(defn blur-input + [input-ref] + (when @input-ref + (rf/dispatch [:chat.ui/set-input-focused false]) + (.blur ^js @input-ref))) + +(defn cancel-reply-message + [input-ref] + (js/setTimeout #(blur-input input-ref) 100) + (rf/dispatch [:chat.ui/set-input-content-height constants/input-height]) + (rf/dispatch [:chat.ui/cancel-message-reply])) + (defn cancel-edit-message - [{:keys [text-value]}] + [text-value input-ref] (reset! text-value "") - (rf/dispatch [:chat.ui/set-input-content-height constants/input-height])) + ;; NOTE: adding a timeout to assure the input is blurred on the next tick + ;; after the `text-value` was cleared. Otherwise the height will be calculated + ;; with the old `text-value`, leading to wrong composer height after blur. + (js/setTimeout #(blur-input input-ref) 100) + (.setNativeProps ^js @input-ref (clj->js {:text ""})) + (rf/dispatch [:chat.ui/set-input-content-height constants/input-height]) + (rf/dispatch [:chat.ui/cancel-message-edit])) (defn count-lines [s] diff --git a/src/status_im2/contexts/chat/composer/view.cljs b/src/status_im2/contexts/chat/composer/view.cljs index 9058053e34..e83a867230 100644 --- a/src/status_im2/contexts/chat/composer/view.cljs +++ b/src/status_im2/contexts/chat/composer/view.cljs @@ -33,7 +33,8 @@ blur-height opacity background-y - theme]} props state] + theme + messages-list-on-layout-finished?]} props state] (let [{:keys [chat-screen-loaded?] :as subscriptions} (utils/init-subs) content-height (reagent/atom (or (:input-content-height ; Actual text height @@ -73,10 +74,9 @@ animations dimensions subscriptions) - (effects/use-edit props state subscriptions) - (effects/use-reply props animations subscriptions) + (effects/use-edit props state subscriptions messages-list-on-layout-finished?) + (effects/use-reply props animations subscriptions messages-list-on-layout-finished?) (effects/update-input-mention props state subscriptions) - (effects/edit-mentions props state subscriptions) (effects/link-previews props state animations subscriptions) (effects/use-images props state animations subscriptions) [:<> @@ -98,8 +98,10 @@ [sub-view/bar] (when chat-screen-loaded? [:<> - [reply/view state] - [edit/view state]]) + [reply/view state (:input-ref props)] + [edit/view + {:text-value (:text-value state) + :input-ref (:input-ref props)}]]) [reanimated/touchable-opacity {:active-opacity 1 :on-press (fn [] @@ -147,11 +149,13 @@ subscriptions]]]]])) (defn composer - [{:keys [insets scroll-to-bottom-fn show-floating-scroll-down-button?]}] + [{:keys [insets scroll-to-bottom-fn show-floating-scroll-down-button? + messages-list-on-layout-finished?]}] (let [window-height (:height (rn/get-window)) theme (quo.theme/use-theme-value) opacity (reanimated/use-shared-value 0) - background-y (reanimated/use-shared-value (- window-height)) ; Y position of background overlay + background-y (reanimated/use-shared-value (- window-height)) ; Y position of background + ; overlay blur-height (reanimated/use-shared-value (+ constants/composer-default-height (:bottom insets))) extra-params {:insets insets @@ -161,7 +165,8 @@ :blur-height blur-height :opacity opacity :background-y background-y - :theme theme} + :theme theme + :messages-list-on-layout-finished? messages-list-on-layout-finished?} props (utils/init-non-reactive-state) state (utils/init-reactive-state)] [rn/view (when platform/ios? {:style {:z-index 1}}) diff --git a/src/status_im2/contexts/chat/messages/list/view.cljs b/src/status_im2/contexts/chat/messages/list/view.cljs index 5311f563db..a1c05ead7a 100644 --- a/src/status_im2/contexts/chat/messages/list/view.cljs +++ b/src/status_im2/contexts/chat/messages/list/view.cljs @@ -298,7 +298,7 @@ (defn f-messages-list-content [{:keys [chat insets scroll-y content-height cover-bg-color keyboard-shown? inner-state-atoms - big-name-visible? animate-topbar-opacity? composer-active? + big-name-visible? animate-topbar-opacity? composer-active? messages-list-on-layout-finished? on-end-reached? animate-topbar-name?]}] (rn/use-effect (fn [] (if (and (not @on-end-reached?) @@ -412,6 +412,15 @@ ;;TODO(rasom) https://github.com/facebook/react-native/issues/30034 :inverted (when platform/ios? true) :on-layout (fn [e] + ;; FIXME: the 1s timeout is to assure all effects with + ;; timeouts that depend on the value are considered. + ;; Hacky, but we're heavily relying on timeouts in the + ;; composer and need to react to differently (e.g. + ;; inside effects/use-edit) when the chat has just + ;; opened and the subsequent times. + (js/setTimeout #(reset! messages-list-on-layout-finished? + true) + 1000) (let [layout-height (oops/oget e "nativeEvent.layout.height")] (reset! messages-view-height layout-height))) diff --git a/src/status_im2/contexts/chat/messages/view.cljs b/src/status_im2/contexts/chat/messages/view.cljs index b6c29ea0df..6a83a6da1d 100644 --- a/src/status_im2/contexts/chat/messages/view.cljs +++ b/src/status_im2/contexts/chat/messages/view.cljs @@ -15,7 +15,7 @@ (defn f-chat [{:keys [show-floating-scroll-down-button? animate-topbar-name? - big-name-visible? animate-topbar-opacity? on-end-reached?] + big-name-visible? animate-topbar-opacity? on-end-reached? messages-list-on-layout-finished?] :as inner-state-atoms}] (let [insets (safe-area/get-insets) scroll-y (reanimated/use-shared-value 0) @@ -53,18 +53,19 @@ :keyboard-vertical-offset (- (:bottom insets))} [list.view/message-list-content-view - {:chat chat - :insets insets - :scroll-y scroll-y - :content-height content-height - :cover-bg-color :turquoise - :keyboard-shown? keyboard-shown - :inner-state-atoms inner-state-atoms - :animate-topbar-name? animate-topbar-name? - :big-name-visible? big-name-visible? - :animate-topbar-opacity? animate-topbar-opacity? - :composer-active? focused? - :on-end-reached? on-end-reached?}] + {:chat chat + :insets insets + :scroll-y scroll-y + :content-height content-height + :cover-bg-color :turquoise + :keyboard-shown? keyboard-shown + :inner-state-atoms inner-state-atoms + :animate-topbar-name? animate-topbar-name? + :big-name-visible? big-name-visible? + :animate-topbar-opacity? animate-topbar-opacity? + :composer-active? focused? + :on-end-reached? on-end-reached? + :messages-list-on-layout-finished? messages-list-on-layout-finished?}] [messages.navigation/navigation-view {:scroll-y scroll-y @@ -86,7 +87,8 @@ [:f> composer.view/composer {:insets insets :scroll-to-bottom-fn list.view/scroll-to-bottom - :show-floating-scroll-down-button? show-floating-scroll-down-button?}] + :show-floating-scroll-down-button? show-floating-scroll-down-button? + :messages-list-on-layout-finished? messages-list-on-layout-finished?}] [contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]))])) (defn chat @@ -99,5 +101,6 @@ :animate-topbar-name? (reagent/atom false) :big-name-visible? (reagent/atom :initial-render) :animate-topbar-opacity? (reagent/atom false) - :on-end-reached? (reagent/atom false)}] + :on-end-reached? (reagent/atom false) + :messages-list-on-layout-finished? (reagent/atom false)}] [:f> f-chat inner-state-atoms]))