diff --git a/Makefile b/Makefile index 7cf8360623..aee9cdaeea 100644 --- a/Makefile +++ b/Makefile @@ -319,7 +319,7 @@ run-visual-test-ios: XCODE_DERIVED_DATA := $(HOME)/Library/Developer/Xcode/Deriv run-visual-test-ios: APPLICATION_NAME := StatusIm-brfnruzfrkkycpbndmdoeyrigthc run-visual-test-ios: export TEST_BINARY_PATH := $(XCODE_DERIVED_DATA)/$(APPLICATION_NAME)/Build/Products/Debug-iphonesimulator/StatusIm.app run-visual-test-ios: ##@test Run tests once in NodeJS - detox test --configuration ios.sim.debug + detox test --configuration ios.sim.debug component-test-watch: export TARGET := clojure component-test-watch: export COMPONENT_TEST := true diff --git a/shadow-cljs.edn b/shadow-cljs.edn index e9a1649b93..730b1b4252 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -82,6 +82,8 @@ :output-dir "target/test" :optimizations :simple :target :node-test + ;; When running tests without a REPL you can uncomment the below line to `make test-watch` a specific file + ;:ns-regexp "status-im.chat.models-test$" :main status-im.test-runner/main ;; set :ui-driven to true to let shadow-cljs inject node-repl :ui-driven true diff --git a/src/status_im/chat/models/images.cljs b/src/status_im/chat/models/images.cljs index 1faffe320c..998dc9ea6e 100644 --- a/src/status_im/chat/models/images.cljs +++ b/src/status_im/chat/models/images.cljs @@ -43,7 +43,7 @@ (defn download-image-http [base64-uri on-success] (-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios? - :path temp-image-url})) + :path temp-image-url})) (.fetch "GET" base64-uri) (.then #(on-success (.path %))) (.catch #(log/error "could not save image")))) @@ -77,8 +77,8 @@ (fn [chat-id] (react/show-image-picker (fn [^js images] - ;; NOTE(Ferossgp): Because we can't highlight the already selected images inside - ;; gallery, we just clean previous state and set all newly picked images + ;; NOTE(Ferossgp): Because we can't highlight the already selected images inside + ;; gallery, we just clean previous state and set all newly picked images (when (and platform/ios? (pos? (count images))) (re-frame/dispatch [:chat.ui/clear-sending-images chat-id])) (doseq [^js result (if platform/ios? @@ -86,8 +86,8 @@ [images])] (resize-and-call (.-path result) #(re-frame/dispatch [:chat.ui/image-selected chat-id (result->id result) %])))) - ;; NOTE(Ferossgp): On android you cannot set max limit on images, when a user - ;; selects too many images the app crashes. + ;; NOTE(Ferossgp): On android you cannot set max limit on images, when a user + ;; selects too many images the app crashes. {:media-type "photo" :multiple platform/ios?}))) @@ -100,13 +100,16 @@ (re-frame/reg-fx ::camera-roll-get-photos - (fn [num] + (fn [[num end-cursor]] (permissions/request-permissions {:permissions [:read-external-storage] :on-allowed (fn [] - (-> (.getPhotos CameraRoll #js {:first num :assetType "Photos" :groupTypes "All"}) - (.then #(re-frame/dispatch [:on-camera-roll-get-photos (:edges (types/js->clj %))])) - (.catch #(log/warn "could not get cameraroll photos"))))}))) + (-> (if end-cursor + (.getPhotos CameraRoll #js {:first num :after end-cursor :assetType "Photos" :groupTypes "All"}) + (.getPhotos CameraRoll #js {:first num :assetType "Photos" :groupTypes "All"})) + (.then #(let [response (types/js->clj %)] + (re-frame/dispatch [:on-camera-roll-get-photos (:edges response) (:page_info response) end-cursor]))) + (.catch #(log/warn "could not get camera roll photos"))))}))) (fx/defn image-captured {:events [:chat.ui/image-captured]} @@ -117,15 +120,32 @@ (not (get images uri))) {::image-selected [uri current-chat-id]}))) +(fx/defn on-end-reached + {:events [:camera-roll/on-end-reached]} + [_ end-cursor loading? has-next-page?] + (when (and (not loading?) has-next-page?) + (re-frame/dispatch [:chat.ui/camera-roll-loading-more true]) + (re-frame/dispatch [:chat.ui/camera-roll-get-photos 20 end-cursor]))) + (fx/defn camera-roll-get-photos {:events [:chat.ui/camera-roll-get-photos]} - [_ num] - {::camera-roll-get-photos num}) + [_ num end-cursor] + {::camera-roll-get-photos [num end-cursor]}) + +(fx/defn camera-roll-loading-more + {:events [:chat.ui/camera-roll-loading-more]} + [{:keys [db]} is-loading] + {:db (assoc db :camera-roll/loading-more is-loading)}) (fx/defn on-camera-roll-get-photos {:events [:on-camera-roll-get-photos]} - [{db :db} photos] - {:db (assoc db :camera-roll-photos (mapv #(get-in % [:node :image :uri]) photos))}) + [{:keys [db] :as cofx} photos page-info end-cursor] + (let [photos_x (when end-cursor (:camera-roll/photos db))] + {:db (-> db + (assoc :camera-roll/photos (concat photos_x (map #(get-in % [:node :image :uri]) photos))) + (assoc :camera-roll/end-cursor (:end_cursor page-info)) + (assoc :camera-roll/has-next-page (:has_next_page page-info)) + (assoc :camera-roll/loading-more false))})) (fx/defn clear-sending-images {:events [:chat.ui/clear-sending-images]} diff --git a/src/status_im/chat/models_test.cljs b/src/status_im/chat/models_test.cljs index 9857043e5c..5bc72ec997 100644 --- a/src/status_im/chat/models_test.cljs +++ b/src/status_im/chat/models_test.cljs @@ -1,7 +1,8 @@ (ns status-im.chat.models-test (:require [cljs.test :refer-macros [deftest is testing]] [status-im.utils.clocks :as utils.clocks] - [status-im.chat.models :as chat])) + [status-im.chat.models :as chat] + [status-im.chat.models.images :as images])) (deftest clear-history-test (let [chat-id "1" @@ -91,3 +92,8 @@ (testing "Pagination info should be reset on navigation" (let [res (chat/navigate-to-chat-nav2 {:db db} chat-id false)] (is (nil? (get-in res [:db :pagination-info chat-id :all-loaded?]))))))) + +(deftest camera-roll-loading-more-test + (let [cofx {:db {:camera-roll/loading-more false}}] + (is (= {:db {:camera-roll/loading-more true}} + (images/camera-roll-loading-more cofx true))))) diff --git a/src/status_im/ui/screens/chat/image/views.cljs b/src/status_im/ui/screens/chat/image/views.cljs index e747c7fb68..7e5febe102 100644 --- a/src/status_im/ui/screens/chat/image/views.cljs +++ b/src/status_im/ui/screens/chat/image/views.cljs @@ -80,7 +80,7 @@ [quo/radio {:value true}]])]])) (defview photos [] - (letsubs [camera-roll-photos [:camera-roll-photos] + (letsubs [camera-roll-photos [:camera-roll/photos] selected [:chats/sending-image] panel-height (reagent/atom nil)] [react/view {:style {:flex 1 diff --git a/src/status_im/ui/screens/status/new/views.cljs b/src/status_im/ui/screens/status/new/views.cljs index 30ea132533..2934b51aae 100644 --- a/src/status_im/ui/screens/status/new/views.cljs +++ b/src/status_im/ui/screens/status/new/views.cljs @@ -33,7 +33,7 @@ :source {:uri uri}}]]) (defview photos [] - (letsubs [camera-roll-photos [:camera-roll-photos]] + (letsubs [camera-roll-photos [:camera-roll/photos]] {:component-did-mount #(re-frame/dispatch [:chat.ui/camera-roll-get-photos 20])} [react/scroll-view {:horizontal true :style {:max-height 88} diff --git a/src/status_im/ui2/screens/chat/composer/images/view.cljs b/src/status_im/ui2/screens/chat/composer/images/view.cljs new file mode 100644 index 0000000000..f484f1bf63 --- /dev/null +++ b/src/status_im/ui2/screens/chat/composer/images/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.ui2.screens.chat.composer.images.view + (:require [react-native.core :as rn] + [status-im.ui2.screens.chat.composer.style :as style] + [quo2.core :as quo2] + [quo2.foundations.colors :as colors] + [re-frame.core :as rf])) + +(defn image [item] + [rn/view + [rn/image {:source {:uri (first item)} + :style style/small-image}] + [rn/touchable-opacity + {:on-press (fn [] (rf/dispatch [:chat.ui/image-unselected (first item)])) + :style style/remove-photo-container} + [quo2/icon :i/close {:color colors/white :size 12}]]]) + +(defn images-list [images] + [rn/flat-list {:key-fn first + :render-fn image + :data images + :horizontal true + :style {:bottom 50 :position :absolute :z-index 5} + :content-container-style {:padding-horizontal 20 :margin-top 12} + :separator [rn/view {:style {:width 12}}]}]) diff --git a/src/status_im/ui2/screens/chat/composer/input.cljs b/src/status_im/ui2/screens/chat/composer/input.cljs index 86ac4ff855..c2981862b6 100644 --- a/src/status_im/ui2/screens/chat/composer/input.cljs +++ b/src/status_im/ui2/screens/chat/composer/input.cljs @@ -102,7 +102,7 @@ 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) (seq text)) + (when (and (empty? prev-text) (or (seq text) sending-image)) (show-send refs)) (when (and (not (get @mentions-enabled? chat-id)) (string/index-of text "@")) diff --git a/src/status_im/ui2/screens/chat/composer/style.cljs b/src/status_im/ui2/screens/chat/composer/style.cljs index 3629896067..be26d5df1f 100644 --- a/src/status_im/ui2/screens/chat/composer/style.cljs +++ b/src/status_im/ui2/screens/chat/composer/style.cljs @@ -79,3 +79,20 @@ (when-not pin? {:position :absolute :left 34 :top 3}))) + +(def remove-photo-container + {:width 14 + :height 14 + :border-radius 7 + :background-color colors/neutral-50 + :position :absolute + :top -7 + :right -7 + :justify-content :center + :align-items :center}) + +(def small-image + {:width 56 + :height 56 + :border-radius 8 + :margin-bottom 20}) diff --git a/src/status_im/ui2/screens/chat/composer/view.cljs b/src/status_im/ui2/screens/chat/composer/view.cljs index c67c4da533..b2e90f2ab8 100644 --- a/src/status_im/ui2/screens/chat/composer/view.cljs +++ b/src/status_im/ui2/screens/chat/composer/view.cljs @@ -4,7 +4,6 @@ [re-frame.core :as re-frame] [quo.components.safe-area :as safe-area] [quo.react-native :as rn :refer [navigation-const]] - [status-im.ui2.screens.chat.composer.style :as styles] [status-im.ui2.screens.chat.composer.reply :as reply] [quo2.components.buttons.button :as quo2.button] [status-im.utils.handlers :refer [ keyboard-height 0) keyboard-height 360) (:top insets) (:status-bar-height @navigation-const)) ; 360 - default height max-height (Math/abs (- max-y 56 (:bottom insets))) ; 56 - top-bar height added-value (if (and (not (seq suggestions)) (or edit reply)) 38 0) ; increased height of input box needed when reply min-y (+ min-y (when (or edit reply) 38)) - y (get-y-value context min-y max-y added-value max-height chat-id suggestions reply) + y (get-y-value context min-y max-y added-value max-height chat-id suggestions reply images) translate-y (reanimated/use-shared-value 0) shared-height (reanimated/use-shared-value min-y) bg-opacity (reanimated/use-shared-value 0) @@ -170,8 +174,10 @@ (quo.react/effect! #(do (when (and @keyboard-was-shown? (not keyboard-shown)) (swap! context assoc :state :min)) - (when blank-composer? + (when (and blank-composer? (not (seq images))) (clean-and-minimize-composer-fn false)) + (when (seq images) + (input/show-send refs)) (reset! keyboard-was-shown? keyboard-shown) (if (#{:max :custom-chat-unavailable} (:state @context)) (set-bg-opacity 1) @@ -185,26 +191,27 @@ [gesture/gesture-detector {:gesture bottom-sheet-gesture} [reanimated/view {:style (reanimated/apply-animations-to-style {:transform [{:translateY translate-y}]} - (styles/input-bottom-sheet window-height))} + (style/input-bottom-sheet window-height))} ;handle - [rn/view {:style (styles/bottom-sheet-handle)}] + [rn/view {:style (style/bottom-sheet-handle)}] [edit/edit-message-auto-focus-wrapper text-input-ref edit clean-and-minimize-composer-fn] [reply/reply-message-auto-focus-wrapper text-input-ref reply] [rn/view {:style {:height (- max-y 80 added-value)}} [input/text-input {:chat-id chat-id :on-content-size-change input-content-change - :sending-image false + :sending-image (seq images) :initial-value initial-value :refs refs :set-active-panel #()}]]]] ;CONTROLS (when-not (seq suggestions) - [rn/view {:style (styles/bottom-sheet-controls insets)} + [rn/view {:style (style/bottom-sheet-controls insets)} [quo2.button/button {:on-press (fn [] (permissions/request-permissions {:permissions [:read-external-storage :write-external-storage] :on-allowed #(re-frame/dispatch [:bottom-sheet/show-sheet - {:content [photo-selector/photo-selector]}]) + {:content (fn [] + (photo-selector/photo-selector chat-id))}]) :on-denied (fn [] (utils/set-timeout #(utils/show-popup (i18n/label :t/error) @@ -219,8 +226,8 @@ [rn/view {:flex 1}] ;;SEND button [rn/view {:ref send-ref - :style (when-not (seq (get @input/input-texts chat-id)) {:width 0 - :right -100})} + :style (when (seq images) {:width 0 + :right -100})} [quo2.button/button {:icon true :size 32 :accessibility-label :send-message-button @@ -231,5 +238,6 @@ [reanimated/view {:style (reanimated/apply-animations-to-style {:opacity bg-opacity :transform [{:translateY bg-bottom}]} - (styles/bottom-sheet-background window-height))}] + (style/bottom-sheet-background window-height))}] + [composer-images/images-list images] [mentions/autocomplete-mentions suggestions text-input-ref]]))])))]) diff --git a/src/status_im/ui2/screens/chat/photo_selector/style.cljs b/src/status_im/ui2/screens/chat/photo_selector/style.cljs index d6d32d3a6e..d067241381 100644 --- a/src/status_im/ui2/screens/chat/photo_selector/style.cljs +++ b/src/status_im/ui2/screens/chat/photo_selector/style.cljs @@ -1,22 +1,12 @@ (ns status-im.ui2.screens.chat.photo-selector.style - (:require [quo2.foundations.colors :as colors])) - -(defn remove-photo-container [] - {:width 14 - :height 14 - :border-radius 7 - :background-color colors/neutral-50 - :position :absolute - :top -7 - :right -7 - :justify-content :center - :align-items :center}) + (:require [quo2.foundations.colors :as colors] + [react-native.platform :as platform])) (defn gradient-container [safe-area] {:width "100%" - :height (+ (:bottom safe-area) 161) + :height (+ (:bottom safe-area) 65) :position :absolute - :bottom 0}) + :bottom (if platform/ios? 0 80)}) (defn buttons-container [safe-area] {:flex-direction :row diff --git a/src/status_im/ui2/screens/chat/photo_selector/view.cljs b/src/status_im/ui2/screens/chat/photo_selector/view.cljs index 0c1b2a5ced..4e46d76e8a 100644 --- a/src/status_im/ui2/screens/chat/photo_selector/view.cljs +++ b/src/status_im/ui2/screens/chat/photo_selector/view.cljs @@ -13,18 +13,6 @@ (def selected (reagent/atom [])) -(defn small-image [item] - [rn/view - [rn/image {:source {:uri item} - :style {:width 56 - :height 56 - :border-radius 8 - :margin-bottom 20}}] - [rn/touchable-opacity - {:on-press (fn [] (reset! selected (vec (remove #(= % item) @selected)))) - :style (style/remove-photo-container)} - [quo2/icon :i/close {:color colors/white :size 12}]]]) - (defn bottom-gradient [] [:f> (fn [] @@ -35,24 +23,14 @@ :start {:x 0 :y 1} :end {:x 0 :y 0} :style (style/gradient-container safe-area)} - [rn/flat-list {:key-fn (fn [item] item) - :render-fn small-image - :data @selected - :horizontal true - :content-container-style {:padding-horizontal 20 :margin-top 12} - :separator [rn/view {:style {:width 12}}]}] - [rn/view {:style (style/buttons-container safe-area)} - [quo2/button {:type :grey - :style {:flex 0.48} - :on-press #(js/alert "Add text: to be implemented")} - (i18n/label :t/add-text)] - [quo2/button {:style {:flex 0.48} - :before :send - :on-press #(do - (rf/dispatch [:chat.ui/send-current-message]) - (reset! selected []) - (rf/dispatch [:bottom-sheet/hide]))} - (str (i18n/label :t/send) " " (when (> (count @selected) 1) (count @selected)))]]])))]) + [quo2/button {:style {:align-self :stretch + :margin-horizontal 20} + :on-press #(do + (doseq [item @selected] + (rf/dispatch [:chat.ui/camera-roll-pick item])) + (reset! selected []) + (rf/dispatch [:bottom-sheet/hide]))} + (i18n/label :t/confirm-selection)]])))]) (defn clear-button [] (when (pos? (count @selected)) @@ -60,17 +38,13 @@ :style (style/clear-container)} [quo2/text {:weight :medium} (i18n/label :t/clear)]])) -(defn image [item index window-width] +(defn image [item index _ {:keys [window-width]}] [rn/touchable-opacity {:active-opacity 1 :on-press (fn [] (if (some #{item} @selected) - (do - (reset! selected (vec (remove #(= % item) @selected))) - (rf/dispatch [:chat.ui/image-unselected item])) - (do - (swap! selected conj item) - (rf/dispatch [:chat.ui/camera-roll-pick item]))))} + (reset! selected (vec (remove #(= % item) @selected))) + (swap! selected conj item)))} [rn/image {:source {:uri item} :style (style/image window-width index)}] (when (some #{item} @selected) @@ -78,31 +52,38 @@ (when (some #{item} @selected) [info-count/info-count (+ (utils/first-index #(= item %) @selected) 1) (style/image-count)])]) -(defn photo-selector [] +(defn photo-selector [chat-id] (rf/dispatch [:chat.ui/camera-roll-get-photos 20]) - [:f> - (fn [] - (let [{window-height :height - window-width :width} (rn/use-window-dimensions) - safe-area (safe-area/use-safe-area) - camera-roll-photos (rf/sub [:camera-roll-photos])] - [rn/view {:style {:height (- window-height (:top safe-area))}} - [rn/touchable-opacity - {:on-press #(js/alert "Camera: not implemented") - :style (style/camera-button-container)} - [quo2/icon :i/camera {:color (colors/theme-colors colors/neutral-100 colors/white)}]] - [rn/view {:style {:flex-direction :row - :position :absolute - :align-self :center}} - [quo2/text {:weight :medium} (i18n/label :t/recent)] - [rn/view {:style (style/chevron-container)} - [quo2/icon :i/chevron-down {:color (colors/theme-colors colors/neutral-100 colors/white)}]]] - [clear-button] - [rn/flat-list {:key-fn (fn [item] item) - :render-fn (fn [item index] (image item index window-width)) - :data camera-roll-photos - :num-columns 3 - :content-container-style {:width "100%" - :padding-bottom (+ (:bottom safe-area) 100)} - :style {:border-radius 20}}] - [bottom-gradient]]))]) + (let [selected-images (keys (get-in (rf/sub [:chat/inputs]) [chat-id :metadata :sending-image]))] + (when selected-images + (reset! selected (vec selected-images))) + [:f> + (fn [] + (let [{window-height :height window-width :width} (rn/use-window-dimensions) + safe-area (safe-area/use-safe-area) + camera-roll-photos (rf/sub [:camera-roll/photos]) + end-cursor (rf/sub [:camera-roll/end-cursor]) + loading? (rf/sub [:camera-roll/loading-more]) + has-next-page? (rf/sub [:camera-roll/has-next-page])] + [rn/view {:style {:height (- window-height (:top safe-area))}} + [rn/touchable-opacity + {:on-press #(js/alert "Camera: not implemented") + :style (style/camera-button-container)} + [quo2/icon :i/camera {:color (colors/theme-colors colors/neutral-100 colors/white)}]] + [rn/view {:style {:flex-direction :row + :position :absolute + :align-self :center}} + [quo2/text {:weight :medium} (i18n/label :t/recent)] + [rn/view {:style (style/chevron-container)} + [quo2/icon :i/chevron-down {:color (colors/theme-colors colors/neutral-100 colors/white)}]]] + [clear-button] + [rn/flat-list {:key-fn (fn [item] item) + :render-fn image + :render-data {:window-width window-width} + :data camera-roll-photos + :num-columns 3 + :content-container-style {:width "100%" + :padding-bottom (+ (:bottom safe-area) 100)} + :style {:border-radius 20} + :on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor loading? has-next-page?])}] + [bottom-gradient]]))])) diff --git a/src/status_im2/subs/root.cljs b/src/status_im2/subs/root.cljs index b4490b44be..c93498c534 100644 --- a/src/status_im2/subs/root.cljs +++ b/src/status_im2/subs/root.cljs @@ -99,7 +99,10 @@ (reg-root-key-sub :selected-participants :selected-participants) (reg-root-key-sub :chat/inputs :chat/inputs) (reg-root-key-sub :chat/memberships :chat/memberships) -(reg-root-key-sub :camera-roll-photos :camera-roll-photos) +(reg-root-key-sub :camera-roll/photos :camera-roll/photos) +(reg-root-key-sub :camera-roll/end-cursor :camera-roll/end-cursor) +(reg-root-key-sub :camera-roll/has-next-page :camera-roll/has-next-page) +(reg-root-key-sub :camera-roll/loading-more :camera-roll/loading-more) (reg-root-key-sub :group-chat/invitations :group-chat/invitations) (reg-root-key-sub :chats/mention-suggestions :chats/mention-suggestions) (reg-root-key-sub :chat/inputs-with-mentions :chat/inputs-with-mentions) diff --git a/translations/en.json b/translations/en.json index 2b2b7c663e..aacdea1552 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1911,5 +1911,6 @@ "try-your-luck-again": "Try your luck again!", "instruction-after-qr-generated": "On your other device, navigate to the Syncing screen and select “Scan sync”", "show-existing-keys": "Show Existing Keys", - "scan-sync-code": "Scan Sync Code" + "scan-sync-code": "Scan Sync Code", + "confirm-selection": "Confirm selection" }