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