Photo Selector 2 (#14487)

* Photo Selector (2)
This commit is contained in:
Omar Basem 2022-12-20 07:36:06 +04:00 committed by GitHub
parent 003b0ffb1e
commit ba41d4b271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 163 additions and 111 deletions

View File

@ -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

View File

@ -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

View File

@ -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]}

View File

@ -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)))))

View File

@ -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

View File

@ -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}

View File

@ -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}}]}])

View File

@ -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 "@"))

View File

@ -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})

View File

@ -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 [<sub]]
@ -17,7 +16,10 @@
[status-im.ui2.screens.chat.photo-selector.view :as photo-selector]
[status-im.utils.utils :as utils]
[i18n.i18n :as i18n]
[status-im.ui2.screens.chat.composer.edit.view :as edit]))
[status-im.ui2.screens.chat.composer.edit.view :as edit]
[utils.re-frame :as rf]
[status-im.ui2.screens.chat.composer.images.view :as composer-images]
[status-im.ui2.screens.chat.composer.style :as style]))
(defn calculate-y [context min-y max-y added-value chat-id]
(let [input-text (:input-text (get (<sub [:chat/inputs]) chat-id))
@ -41,8 +43,9 @@
mentions-translate-value (if should-translate? (min min-value (- mentions-height (- max-height text-height))) mentions-height)]
(when (or (< y max-y) should-translate?) mentions-translate-value)))
(defn get-y-value [context min-y max-y added-value max-height chat-id suggestions reply]
(defn get-y-value [context min-y max-y added-value max-height chat-id suggestions reply images]
(let [y (calculate-y context min-y max-y added-value chat-id)
y (+ y (when (seq images) 80))
y-with-mentions (calculate-y-with-mentions y max-y max-height chat-id suggestions reply)]
(+ y (when (seq suggestions) y-with-mentions))))
@ -146,13 +149,14 @@
(let [reply (<sub [:chats/reply-message])
edit (<sub [:chats/edit-message])
suggestions (<sub [:chat/mention-suggestions])
images (get-in (rf/sub [:chat/inputs]) [chat-id :metadata :sending-image])
{window-height :height} (rn/use-window-dimensions)
{:keys [keyboard-shown keyboard-height]} (rn/use-keyboard)
max-y (- window-height (if (> 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]]))])))])

View File

@ -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

View File

@ -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]]))]))

View File

@ -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)

View File

@ -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"
}