From a4bc18ee3faea327e3d68a53e8c1fdd13468e672 Mon Sep 17 00:00:00 2001 From: flexsurfer Date: Thu, 1 Jun 2023 10:35:57 +0200 Subject: [PATCH] improve photo-selector and adjust according to the latest designs (#16053) --- src/quo2/components/buttons/button.cljs | 17 +- src/quo2/components/dropdowns/dropdown.cljs | 173 +------------- src/react_native/cameraroll.cljs | 16 ++ src/react_native/core.cljs | 1 + src/react_native/gesture.cljs | 8 +- src/status_im/chat/models/images.cljs | 220 +----------------- src/status_im/events.cljs | 2 +- src/status_im/ui/components/permissions.cljs | 34 --- src/status_im/ui/screens/browser/views.cljs | 2 +- src/status_im2/contexts/chat/events_test.cljs | 6 - .../photo_selector/album_selector/view.cljs | 49 ++-- .../contexts/chat/photo_selector/events.cljs | 146 ++++++++++++ .../contexts/chat/photo_selector/style.cljs | 13 +- .../contexts/chat/photo_selector/view.cljs | 204 +++++++--------- .../quo_preview/dropdowns/dropdown.cljs | 114 ++++----- src/status_im2/events.cljs | 3 +- src/status_im2/navigation/screens.cljs | 5 - 17 files changed, 368 insertions(+), 645 deletions(-) create mode 100644 src/react_native/cameraroll.cljs delete mode 100644 src/status_im/ui/components/permissions.cljs create mode 100644 src/status_im2/contexts/chat/photo_selector/events.cljs diff --git a/src/quo2/components/buttons/button.cljs b/src/quo2/components/buttons/button.cljs index ada2a58cd2..16a546f18a 100644 --- a/src/quo2/components/buttons/button.cljs +++ b/src/quo2/components/buttons/button.cljs @@ -220,10 +220,10 @@ only icon [button {:icon true} :i/close-circle]" [_ _] - (let [pressed (reagent/atom false)] + (let [pressed-in (reagent/atom false)] (fn - [{:keys [on-press disabled type size before after above - width customization-color override-theme override-background-color + [{:keys [on-press disabled type size before after above icon-secondary-no-color + width customization-color override-theme override-background-color pressed on-long-press accessibility-label icon icon-no-color style inner-style test-ID] :or {type :primary size 40 @@ -234,17 +234,17 @@ [(or override-theme (theme/get-theme)) type]) - state (cond disabled :disabled - @pressed :pressed - :else :default) + state (cond disabled :disabled + (or @pressed-in pressed) :pressed + :else :default) icon-size (when (= 24 size) 12) icon-secondary-color (or icon-secondary-color icon-color)] [rn/touchable-without-feedback (merge {:test-ID test-ID :disabled disabled :accessibility-label accessibility-label - :on-press-in #(reset! pressed true) - :on-press-out #(reset! pressed nil)} + :on-press-in #(reset! pressed-in true) + :on-press-out #(reset! pressed-in nil)} (when on-press {:on-press on-press}) (when on-long-press @@ -306,5 +306,6 @@ [quo2.icons/icon after {:container-style {:margin-left 4 :margin-right (if (= size 40) 12 8)} + :no-color icon-secondary-no-color :color icon-secondary-color :size icon-size}]])]]])))) diff --git a/src/quo2/components/dropdowns/dropdown.cljs b/src/quo2/components/dropdowns/dropdown.cljs index c7364f2cc9..f3aca87f28 100644 --- a/src/quo2/components/dropdowns/dropdown.cljs +++ b/src/quo2/components/dropdowns/dropdown.cljs @@ -1,165 +1,14 @@ (ns quo2.components.dropdowns.dropdown - (:require [quo2.components.icon :as icons] - [quo2.components.markdown.text :as text] - [quo2.foundations.colors :as colors] - [react-native.core :as rn] - [react-native.reanimated :as reanimated] - [reagent.core :as reagent])) - -(defn apply-anim - [dd-height val] - (reanimated/animate-shared-value-with-delay dd-height - val - 300 - :easing1 - 0)) - -(def sizes - {:big {:icon-size 20 - :font {:font-size :paragraph-1} - :height 40 - :padding {:padding-with-icon {:padding-vertical 9 - :padding-horizontal 12} - :padding-with-no-icon {:padding-vertical 9 - :padding-left 12 - :padding-right 8}}} - :medium {:icon-size 20 - :font {:font-size :paragraph-1} - :height 32 - :padding {:padding-with-icon {:padding-vertical 5 - :padding-horizontal 8} - :padding-with-no-icon {:padding-vertical 5 - :padding-left 12 - :padding-right 8}}} - :small {:icon-size 15 - :font {:font-size :paragraph-2} - :height 24 - :padding {:padding-with-icon {:padding-vertical 3 - :padding-horizontal 6} - :padding-with-no-icon {:padding-vertical 3 - :padding-horizontal 6}}}}) -(defn color-by-10 - [color] - (colors/alpha color 0.6)) - -(defn dropdown-comp - [{:keys [icon dd-height size disabled? dd-color use-border? border-color]}] - (let [dark? (colors/dark?) - {:keys [width height width-with-icon padding font icon-size]} (size sizes) - {:keys [padding-with-icon padding-with-no-icon]} padding - font-size (:font-size font) - spacing (case size - :big 4 - :medium 2 - :small 2) - open? (reagent/atom false)] - (fn [] - [rn/touchable-opacity - (cond-> - {:on-press (fn [] - (if (swap! open? not) - (apply-anim dd-height 120) - (apply-anim dd-height 0))) - :style (cond-> - (merge - (if icon - padding-with-icon - padding-with-no-icon) - {:width (if icon - width-with-icon - width) - :height height - :border-radius (case size - :big 12 - :medium 10 - :small 8) - :flex-direction :row - :align-items :center - :background-color (if @open? - dd-color - (color-by-10 dd-color))}) - use-border? (assoc :border-width 1 - :border-color (if @open? - border-color - (color-by-10 border-color))))} - disabled? (assoc-in [:style :opacity] 0.3) - disabled? (assoc :disabled true)) - (when icon - [icons/icon icon - {:no-color true - :size 20 - :container-style {:margin-right spacing - :margin-top 1 - :width icon-size - :height icon-size}}]) - [text/text - {:size font-size - :weight :medium - :font :font-medium - :color :main} "Dropdown"] - [icons/icon - (if @open? - (if dark? - :main-icons/pullup-dark - :main-icons/pullup) - (if dark? - :main-icons/dropdown-dark - :main-icons/dropdown)) - {:size 20 - :no-color true - :container-style {:width (+ icon-size 3) - :border-radius 20 - :margin-left (if (= :small size) - 2 - 4) - :margin-top 1 - :height (+ icon-size 4)}}]]))) - -(defn items-comp - [{:keys [items on-select]}] - (let [items-count (count items)] - [rn/scroll-view - {:horizontal false - :nestedScrollEnabled true} - (doall - (map-indexed (fn [index item] - [rn/touchable-opacity - {:key (str item index) - :style {:padding 4 - :border-bottom-width (if (= index (- items-count 1)) - 0 - 1) - :border-color (colors/theme-colors - colors/neutral-100 - colors/white) - :text-align :center} - :on-press #(on-select item)} - [text/text {:style {:text-align :center}} item]]) - items))])) - -(defn- f-dropdown - [{:keys [items icon text default-item on-select size disabled? border-color use-border? dd-color]}] - (let [dd-height (reanimated/use-shared-value 0)] - [rn/view {:style {:flex-grow 1}} - [dropdown-comp - {:items items - :icon icon - :disabled? disabled? - :size size - :dd-color dd-color - :text text - :border-color (colors/custom-color-by-theme border-color 50 60) - :use-border? use-border? - :default-item default-item - :dd-height dd-height}] - [reanimated/view - {:style (reanimated/apply-animations-to-style - {:height dd-height} - {})} - [items-comp - {:items items - :on-select on-select}]]])) + (:require [quo2.components.buttons.button :as button])) (defn dropdown - [params] - [:f> f-dropdown params]) + [_ _] + (fn [{:keys [on-change selected] :as opts} children] + [button/button + (merge + opts + {:after (if selected :i/pullup :i/dropdown) + :icon-secondary-no-color true + :pressed selected + :on-press #(when on-change (on-change selected))}) + children])) diff --git a/src/react_native/cameraroll.cljs b/src/react_native/cameraroll.cljs new file mode 100644 index 0000000000..53b19f76cd --- /dev/null +++ b/src/react_native/cameraroll.cljs @@ -0,0 +1,16 @@ +(ns react-native.cameraroll + (:require ["@react-native-community/cameraroll" :as CameraRoll] + [utils.transforms :as transforms] + [taoensso.timbre :as log])) + +(defn get-photos + [opts callback] + (-> (.getPhotos CameraRoll (clj->js opts)) + (.then #(callback (transforms/js->clj %))) + (.catch #(log/warn "could not get camera roll photos" %)))) + +(defn get-albums + [opts callback] + (-> (.getAlbums CameraRoll (clj->js opts)) + (.then #(callback (transforms/js->clj %))) + (.catch #(log/warn "could not get camera roll albums" %)))) diff --git a/src/react_native/core.cljs b/src/react_native/core.cljs index 03b2332aa5..5ea42c5c85 100644 --- a/src/react_native/core.cljs +++ b/src/react_native/core.cljs @@ -17,6 +17,7 @@ (def view (reagent/adapt-react-class (.-View ^js react-native))) (def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native))) (def image (reagent/adapt-react-class (.-Image ^js react-native))) +(defn image-get-size [uri callback] (.getSize ^js (.-Image ^js react-native) uri callback)) (def text (reagent/adapt-react-class (.-Text ^js react-native))) (def text-input (reagent/adapt-react-class (.-TextInput ^js react-native))) diff --git a/src/react_native/gesture.cljs b/src/react_native/gesture.cljs index bd7aa2aa15..e755ef7e7b 100644 --- a/src/react_native/gesture.cljs +++ b/src/react_native/gesture.cljs @@ -91,7 +91,7 @@ [flat-list (merge props {:data data - :render-fn (fn [item] - (if (:header? item) - (render-section-header-fn item) - (render-fn item)))})])) + :render-fn (fn [p1 p2 p3 p4] + (if (:header? p1) + [render-section-header-fn p1 p2 p3 p4] + [render-fn p1 p2 p3 p4]))})])) diff --git a/src/status_im/chat/models/images.cljs b/src/status_im/chat/models/images.cljs index 5190ed3ed3..aac10ff4ec 100644 --- a/src/status_im/chat/models/images.cljs +++ b/src/status_im/chat/models/images.cljs @@ -1,47 +1,17 @@ (ns status-im.chat.models.images (:require ["@react-native-community/cameraroll" :as CameraRoll] ["react-native-blob-util" :default ReactNativeBlobUtil] - [clojure.string :as string] [re-frame.core :as re-frame] [utils.i18n :as i18n] - [status-im.ui.components.permissions :as permissions] + [react-native.permissions :as permissions] [status-im.ui.components.react :as react] [status-im2.config :as config] [status-im.utils.fs :as fs] [utils.re-frame :as rf] - [status-im.utils.image-processing :as image-processing] [status-im.utils.platform :as platform] - [status-im.utils.types :as types] [status-im.utils.utils :as utils] [taoensso.timbre :as log])) -(def maximum-image-size-px 2000) - -(defn- resize-and-call - [uri cb] - (react/image-get-size - uri - (fn [width height] - (let [resize? (> (max width height) maximum-image-size-px)] - (image-processing/resize - uri - (if resize? maximum-image-size-px width) - (if resize? maximum-image-size-px height) - 60 - (fn [^js resized-image] - (let [path (.-path resized-image) - path (if (string/starts-with? path "file") path (str "file://" path))] - (cb {:resized-uri path - :width width - :height height}))) - #(log/error "could not resize image" %)))))) - -(defn result->id - [^js result] - (if platform/ios? - (.-localIdentifier result) - (.-path result))) - (def temp-image-url (str (fs/cache-dir) "/StatusIm_Image.jpeg")) (defn download-image-http @@ -78,108 +48,6 @@ #(re-frame/dispatch [:chat.ui/image-captured current-chat-id (.-path %)]) {}))) -(re-frame/reg-fx - ::chat-open-image-picker - (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 - (when (and platform/ios? (pos? (count images))) - (re-frame/dispatch [:chat.ui/clear-sending-images chat-id])) - (doseq [^js result (if platform/ios? - (take config/max-images-batch images) - [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. - {:media-type "photo" - :multiple platform/ios?}))) - -(re-frame/reg-fx - ::image-selected - (fn [[image chat-id]] - (resize-and-call - (:uri image) - #(re-frame/dispatch [:chat.ui/image-selected chat-id image %])))) - -(re-frame/reg-fx - ::camera-roll-get-photos - (fn [[num end-cursor album]] - (permissions/request-permissions - {:permissions [:read-external-storage] - :on-allowed (fn [] - (-> (if end-cursor - (.getPhotos - CameraRoll - #js - {:first num - :after end-cursor - :assetType "Photos" - :groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums") - :groupName (when (not= album (i18n/label :t/recent)) album) - :include (clj->js ["imageSize"])}) - (.getPhotos - CameraRoll - #js - {:first num - :assetType "Photos" - :groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums") - :groupName (when (not= album (i18n/label :t/recent)) album) - :include (clj->js ["imageSize"])})) - (.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"))))}))) - -(re-frame/reg-fx - :chat.ui/camera-roll-get-albums - (fn [] - (let [albums (atom [{:title :smart-albums :data []} - {:title (i18n/label :t/my-albums) :data []}])] - ;; Get the "recent" album first - (-> - (.getPhotos CameraRoll - #js - {:first 1 - :groupTypes "All"}) - (.then - (fn [res] - (let [recent-album {:title (i18n/label :t/recent) - :count "--" - :uri (get-in (first (:edges (types/js->clj res))) [:node :image :uri])}] - (swap! albums update-in [0 :data] conj recent-album) - ;; Get albums, then loop over albums and get each one's cover (first photo) - (-> - (.getAlbums CameraRoll #js {:assetType "All"}) - (.then - (fn [response] - (let [response (types/js->clj response)] - (reduce - (fn [_ album] - (-> - (.getPhotos - CameraRoll - #js - {:first 1 - :groupTypes "Albums" - :groupName (:title album)}) - (.then (fn [res] - (let [uri (get-in (first (:edges (types/js->clj res))) - [:node :image :uri])] - (swap! albums update-in [1 :data] conj (merge album {:uri uri})) - (when (= (count (get-in @albums [1 :data])) (count response)) - (swap! albums update-in - [1 :data] - #(->> % - (sort-by :title))) - (re-frame/dispatch [:on-camera-roll-get-albums @albums]))))))) - nil - response)))) - (.catch #(log/warn "could not get camera roll albums")))))))))) - - (rf/defn image-captured {:events [:chat.ui/image-captured]} [{:keys [db]} chat-id uri] @@ -189,81 +57,17 @@ (not (get images uri))) {::image-selected [uri current-chat-id]}))) -(rf/defn on-end-reached - {:events [:camera-roll/on-end-reached]} - [_ end-cursor selected-album 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 selected-album]))) - -(rf/defn camera-roll-get-photos - {:events [:chat.ui/camera-roll-get-photos]} - [_ num end-cursor selected-album] - {::camera-roll-get-photos [num end-cursor selected-album]}) - -(rf/defn camera-roll-get-albums - {:events [:chat.ui/camera-roll-get-albums]} - [_] - {:chat.ui/camera-roll-get-albums []}) - -(rf/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)}) - -(rf/defn on-camera-roll-get-photos - {:events [:on-camera-roll-get-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]) 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))})) - -(rf/defn on-camera-roll-get-albums - {:events [:on-camera-roll-get-albums]} - [{:keys [db]} albums] - {:db (-> db - (assoc :camera-roll/albums albums) - (assoc :camera-roll/loading-albums false))}) - (rf/defn clear-sending-images {:events [:chat.ui/clear-sending-images]} [{:keys [db]}] {:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})}) -(rf/defn cancel-sending-image - {:events [:chat.ui/cancel-sending-image]} - [{:keys [db] :as cofx} chat-id] - (clear-sending-images cofx)) - -(rf/defn image-selected - {:events [:chat.ui/image-selected]} - [{:keys [db]} current-chat-id original {:keys [resized-uri width height]}] - {:db - (update-in db - [:chat/inputs current-chat-id :metadata :sending-image (:uri original)] - merge - original - {:resized-uri resized-uri - :width width - :height height})}) - (rf/defn image-unselected {:events [:chat.ui/image-unselected]} [{:keys [db]} original] (let [current-chat-id (:current-chat-id db)] {:db (update-in db [:chat/inputs current-chat-id :metadata :sending-image] dissoc (:uri original))})) -(rf/defn chat-open-image-picker - {:events [:chat.ui/open-image-picker]} - [{:keys [db]} chat-id] - (let [current-chat-id (or chat-id (:current-chat-id db)) - images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] - (when (< (count images) config/max-images-batch) - {::chat-open-image-picker current-chat-id}))) - (rf/defn chat-show-image-picker-camera {:events [:chat.ui/show-image-picker-camera]} [{:keys [db]} chat-id] @@ -271,25 +75,3 @@ images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] (when (< (count images) config/max-images-batch) {::chat-open-image-picker-camera current-chat-id}))) - -(rf/defn camera-roll-pick - {:events [:chat.ui/camera-roll-pick]} - [{:keys [db]} image chat-id] - (let [current-chat-id (or chat-id (:current-chat-id db)) - images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] - (if (get-in db [:chats current-chat-id :timeline?]) - {:db (assoc-in db [:chat/inputs current-chat-id :metadata :sending-image] {}) - ::image-selected [image current-chat-id]} - (when (and (< (count images) config/max-images-batch) - (not (some #(= (:uri image) (:uri %)) images))) - {::image-selected [image current-chat-id]})))) - -(rf/defn camera-roll-select-album - {:events [:chat.ui/camera-roll-select-album]} - [{:keys [db]} album] - {:db (assoc db :camera-roll/selected-album album)}) - -(rf/defn save-image-to-gallery - {:events [:chat.ui/save-image-to-gallery]} - [_ base64-uri] - {::save-image-to-gallery base64-uri}) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 9bab2f3626..48be2fb83f 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -36,7 +36,7 @@ status-im.signals.core status-im.stickers.core status-im.transport.core - [status-im.ui.components.permissions :as permissions] + [react-native.permissions :as permissions] [status-im.ui.components.react :as react] status-im.ui.screens.privacy-and-security-settings.events [status-im.utils.dimensions :as dimensions] diff --git a/src/status_im/ui/components/permissions.cljs b/src/status_im/ui/components/permissions.cljs deleted file mode 100644 index ca3ce31bb8..0000000000 --- a/src/status_im/ui/components/permissions.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.ui.components.permissions - (:require ["react-native-permissions" :refer (requestMultiple PERMISSIONS RESULTS)] - [status-im.utils.platform :as platform])) - -(def permissions-map - {:read-external-storage (cond - platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS))) - :write-external-storage (cond - platform/low-device? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS))) - :camera (cond - platform/android? (.-CAMERA (.-ANDROID PERMISSIONS)) - platform/ios? (.-CAMERA (.-IOS PERMISSIONS))) - :record-audio (cond - platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS)) - platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))}) - -(defn all-granted? - [permissions] - (let [permission-vals (distinct (vals permissions))] - (and (= (count permission-vals) 1) - (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals)))))) - -(defn request-permissions - [{:keys [permissions on-allowed on-denied] - :or {on-allowed #() - on-denied #()}}] - (let [permissions (remove nil? (mapv #(get permissions-map %) permissions))] - (if (empty? permissions) - (on-allowed) - (-> (requestMultiple (clj->js permissions)) - (.then #(if (all-granted? (js->clj %)) - (on-allowed) - (on-denied))) - (.catch on-denied))))) diff --git a/src/status_im/ui/screens/browser/views.cljs b/src/status_im/ui/screens/browser/views.cljs index a1c7921c38..dc7ce38525 100644 --- a/src/status_im/ui/screens/browser/views.cljs +++ b/src/status_im/ui/screens/browser/views.cljs @@ -11,7 +11,7 @@ [status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.components.connectivity.view :as connectivity] [status-im.ui.components.icons.icons :as icons] - [status-im.ui.components.permissions :as components.permissions] + [react-native.permissions :as components.permissions] [status-im.ui.components.react :as react] [status-im.ui.components.tooltip.views :as tooltip] [status-im.ui.components.webview :as components.webview] diff --git a/src/status_im2/contexts/chat/events_test.cljs b/src/status_im2/contexts/chat/events_test.cljs index 1673e6e97a..94d5775f0d 100644 --- a/src/status_im2/contexts/chat/events_test.cljs +++ b/src/status_im2/contexts/chat/events_test.cljs @@ -1,7 +1,6 @@ (ns status-im2.contexts.chat.events-test (:require [cljs.test :refer-macros [deftest is testing]] [status-im2.contexts.chat.events :as chat] - [status-im.chat.models.images :as images] [status-im.utils.clocks :as utils.clocks])) (deftest clear-history-test @@ -90,8 +89,3 @@ (testing "Pagination info should be reset on navigation" (let [res (chat/navigate-to-chat {:db db} chat-id)] (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_im2/contexts/chat/photo_selector/album_selector/view.cljs b/src/status_im2/contexts/chat/photo_selector/album_selector/view.cljs index 3f5e2f7d08..e99ee77400 100644 --- a/src/status_im2/contexts/chat/photo_selector/album_selector/view.cljs +++ b/src/status_im2/contexts/chat/photo_selector/album_selector/view.cljs @@ -6,16 +6,16 @@ [utils.i18n :as i18n] [utils.re-frame :as rf] [quo2.foundations.colors :as colors] - [status-im2.contexts.chat.photo-selector.view :refer [album-title]] [status-im2.contexts.chat.photo-selector.album-selector.style :as style])) -(defn album - [{:keys [title count uri]} index _ selected-album] +(defn render-album + [{:keys [title count uri]} index _ {:keys [album? selected-album]}] (let [selected? (= selected-album title)] [rn/touchable-opacity {:on-press (fn [] (rf/dispatch [:chat.ui/camera-roll-select-album title]) - (rf/dispatch [:navigate-back])) + (rf/dispatch [:photo-selector/get-photos-for-selected-album]) + (reset! album? false)) :style (style/album-container selected?) :accessibility-label (str "album-" index)} [rn/image @@ -31,7 +31,7 @@ [quo/text {:size :paragraph-2 :style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}} - (str count " " (i18n/label :t/images))]] + (when count (str count " " (i18n/label :t/images)))]] (when selected? [rn/view {:style {:position :absolute @@ -39,9 +39,11 @@ [quo/icon :i/check {:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60)}]])])) +(def no-title "no-title") + (defn section-header [{:keys [title]}] - (when (not= title "smart-albums") + (when-not (= title no-title) [quo/divider-label {:label title :container-style style/divider}])) @@ -51,22 +53,19 @@ (str (:title item) index)) (defn album-selector - [{:keys [on-scroll]}] - (rf/dispatch [:chat.ui/camera-roll-get-albums]) - (fn [{:keys [scroll-enabled]}] - (let [albums (rf/sub [:camera-roll/albums]) - selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] - [rn/view {:style {:padding-top 20}} - [album-title false] - [gesture/section-list - {:data albums - :render-fn album - :render-data selected-album - :sections albums - :sticky-section-headers-enabled false - :render-section-header-fn section-header - :style {:margin-top 12} - :content-container-style {:padding-bottom 40} - :key-fn key-fn - :scroll-enabled @scroll-enabled - :on-scroll on-scroll}]]))) + [{:keys [scroll-enabled on-scroll]} album? selected-album] + (let [albums (rf/sub [:camera-roll/albums]) + albums-sections [{:title no-title :data (:smart-albums albums)} + {:title (i18n/label :t/my-albums) :data (:my-albums albums)}]] + [gesture/section-list + {:data albums-sections + :sections albums-sections + :render-data {:album? album? :selected-album selected-album} + :render-fn render-album + :sticky-section-headers-enabled false + :render-section-header-fn section-header + :content-container-style {:padding-top 64 + :padding-bottom 40} + :key-fn key-fn + :scroll-enabled @scroll-enabled + :on-scroll on-scroll}])) diff --git a/src/status_im2/contexts/chat/photo_selector/events.cljs b/src/status_im2/contexts/chat/photo_selector/events.cljs new file mode 100644 index 0000000000..320f181e70 --- /dev/null +++ b/src/status_im2/contexts/chat/photo_selector/events.cljs @@ -0,0 +1,146 @@ +(ns status-im2.contexts.chat.photo-selector.events + (:require [react-native.cameraroll :as cameraroll] + [clojure.string :as string] + [re-frame.core :as re-frame] + [utils.i18n :as i18n] + [react-native.permissions :as permissions] + [status-im2.config :as config] + [utils.re-frame :as rf] + [status-im.utils.image-processing :as image-processing] + [taoensso.timbre :as log] + [react-native.core :as rn])) + +(def maximum-image-size-px 2000) + +(defn- resize-photo + [uri callback] + (rn/image-get-size + uri + (fn [width height] + (let [resize? (> (max width height) maximum-image-size-px)] + (image-processing/resize + uri + (if resize? maximum-image-size-px width) + (if resize? maximum-image-size-px height) + 60 + (fn [^js resized-image] + (let [path (.-path resized-image) + path (if (string/starts-with? path "file") path (str "file://" path))] + (callback {:resized-uri path + :width width + :height height}))) + #(log/error "could not resize image" %)))))) + +(re-frame/reg-fx + :camera-roll-request-permissions-and-get-photos + (fn [[num end-cursor album]] + (permissions/request-permissions + {:permissions [:read-external-storage] + :on-allowed + (fn [] + (cameraroll/get-photos + (merge {:first num + :assetType "Photos" + :groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums") + :groupName (when (not= album (i18n/label :t/recent)) album) + :include ["imageSize"]} + (when end-cursor + {:after end-cursor})) + #(re-frame/dispatch [:on-camera-roll-get-photos (:edges %) (:page_info %) end-cursor])))}))) + +(re-frame/reg-fx + :camera-roll-image-selected + (fn [[image chat-id]] + (resize-photo (:uri image) #(re-frame/dispatch [:photo-selector/image-selected chat-id image %])))) + +(defn get-albums + [callback] + (let [albums (atom {:smart-albums [] + :my-albums []})] + ;; Get the "recent" album first + (cameraroll/get-photos + {:first 1 :groupTypes "All"} + (fn [res-recent] + (swap! albums assoc + :smart-albums + [{:title (i18n/label :t/recent) + :uri (get-in (first (:edges res-recent)) [:node :image :uri])}]) + ;; Get albums, then loop over albums and get each one's cover (first photo) + (cameraroll/get-albums + {:assetType "All"} + (fn [res-albums] + (let [response-count (count res-albums)] + (if (pos? response-count) + (doseq [album res-albums] + (cameraroll/get-photos + {:first 1 :groupTypes "Albums" :groupName (:title album)} + (fn [res] + (let [uri (get-in (first (:edges res)) [:node :image :uri])] + (swap! albums update :my-albums conj (merge album {:uri uri})) + (when (= (count (:my-albums @albums)) response-count) + (swap! albums update :my-albums #(sort-by :title %)) + (callback @albums)))))) + (callback @albums))))))))) + +(re-frame/reg-fx + :camera-roll-get-albums + (fn [] + (get-albums #(re-frame/dispatch [:on-camera-roll-get-albums %])))) + +(rf/defn on-camera-roll-get-albums + {:events [:on-camera-roll-get-albums]} + [{:keys [db]} albums] + {:db (assoc db :camera-roll/albums albums)}) + +(rf/defn camera-roll-get-albums + {:events [:photo-selector/camera-roll-get-albums]} + [_] + {:camera-roll-get-albums nil}) + +(rf/defn camera-roll-select-album + {:events [:chat.ui/camera-roll-select-album]} + [{:keys [db]} album] + {:db (assoc db :camera-roll/selected-album album)}) + +(rf/defn image-selected + {:events [:photo-selector/image-selected]} + [{:keys [db]} current-chat-id original {:keys [resized-uri width height]}] + {:db + (update-in db + [:chat/inputs current-chat-id :metadata :sending-image (:uri original)] + merge + original + {:resized-uri resized-uri + :width width + :height height})}) + +(rf/defn on-camera-roll-get-photos + {:events [:on-camera-roll-get-photos]} + [{:keys [db]} 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]) 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))})) + +(rf/defn get-photos-for-selected-album + {:events [:photo-selector/get-photos-for-selected-album]} + [{:keys [db]} end-cursor] + {:camera-roll-request-permissions-and-get-photos [21 end-cursor + (or (:camera-roll/selected-album db) + (i18n/label :t/recent))]}) + +(rf/defn camera-roll-loading-more + {:events [:photo-selector/camera-roll-loading-more]} + [{:keys [db]} is-loading] + {:db (assoc db :camera-roll/loading-more is-loading)}) + +(rf/defn camera-roll-pick + {:events [:photo-selector/camera-roll-pick]} + [{:keys [db]} image chat-id] + (let [current-chat-id (or chat-id (:current-chat-id db)) + images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] + (when (and (< (count images) config/max-images-batch) + (not (some #(= (:uri image) (:uri %)) images))) + {:camera-roll-image-selected [image current-chat-id]}))) diff --git a/src/status_im2/contexts/chat/photo_selector/style.cljs b/src/status_im2/contexts/chat/photo_selector/style.cljs index fc563f2db8..eb334ec1a5 100644 --- a/src/status_im2/contexts/chat/photo_selector/style.cljs +++ b/src/status_im2/contexts/chat/photo_selector/style.cljs @@ -15,18 +15,13 @@ :flex-direction :row :left 0 :right 0 - :top 0 + :top 20 :justify-content :center :z-index 1}) -(defn clear-container - [] - {:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80) - :padding-horizontal 12 - :padding-vertical 5 - :border-radius 10 - :position :absolute - :right 20}) +(def clear-container + {:position :absolute + :right 20}) (defn close-button-container [] diff --git a/src/status_im2/contexts/chat/photo_selector/view.cljs b/src/status_im2/contexts/chat/photo_selector/view.cljs index b62e58dcda..a61b6ac2b0 100644 --- a/src/status_im2/contexts/chat/photo_selector/view.cljs +++ b/src/status_im2/contexts/chat/photo_selector/view.cljs @@ -1,150 +1,126 @@ (ns status-im2.contexts.chat.photo-selector.view (:require [react-native.gesture :as gesture] + [react-native.safe-area :as safe-area] [react-native.platform :as platform] - [status-im2.constants :as constants] [utils.i18n :as i18n] + [utils.re-frame :as rf] [quo2.components.notifications.info-count :as info-count] [quo2.core :as quo] [quo2.foundations.colors :as colors] [react-native.core :as rn] [react-native.linear-gradient :as linear-gradient] [reagent.core :as reagent] + [status-im2.constants :as constants] [status-im2.contexts.chat.photo-selector.style :as style] - [status-im.utils.core :as utils] - [quo.react] - [utils.re-frame :as rf])) + [status-im2.contexts.chat.photo-selector.album-selector.view :as album-selector] + utils.collection)) + +(defn show-toast + [] + (rf/dispatch [:toasts/upsert + {:id :random-id + :icon :info + :icon-color colors/danger-50-opa-40 + :container-style {:top (when platform/ios? 20)} + :text (i18n/label :t/only-6-images)}])) (defn on-press-confirm-selection - [selected] + [selected close] (rf/dispatch [:chat.ui/clear-sending-images]) - (doseq [item @selected] - (rf/dispatch [:chat.ui/camera-roll-pick item])) - (reset! selected []) - (rf/dispatch [:navigate-back])) + (doseq [item selected] + (rf/dispatch [:photo-selector/camera-roll-pick item])) + (close)) -(defn bottom-gradient - [selected-images insets selected] - (when (or (seq @selected) (seq selected-images)) +(defn confirm-button + [selected-images sending-image close] + (when (not= selected-images sending-image) [linear-gradient/linear-gradient {:colors [:black :transparent] :start {:x 0 :y 1} :end {:x 0 :y 0} - :style (style/gradient-container (:bottom insets))} + :style (style/gradient-container (safe-area/get-bottom))} [quo/button {:style {:align-self :stretch :margin-horizontal 20 :margin-top 12} - :on-press #(on-press-confirm-selection selected) + :on-press #(on-press-confirm-selection selected-images close) :accessibility-label :confirm-selection} (i18n/label :t/confirm-selection)]])) (defn clear-button - [selected] - (when (seq @selected) - [rn/touchable-opacity - {:on-press #(reset! selected []) - :style (style/clear-container) - :accessibility-label :clear} - [quo/text {:weight :medium} (i18n/label :t/clear)]])) + [album? selected] + (when (and (not album?) (seq @selected)) + [rn/view {:style style/clear-container} + [quo/button + {:type :grey + :size 32 + :accessibility-label :clear + :on-press #(reset! selected [])} + (i18n/label :t/clear)]])) (defn remove-selected [coll item] (vec (remove #(= (:uri item) (:uri %)) coll))) -(defn image +(defn render-image [item index _ {:keys [window-width selected]}] - [rn/touchable-opacity - {:active-opacity 1 - :on-press (fn [] - (if (some #(= (:uri item) (:uri %)) @selected) - (swap! selected remove-selected item) - (if (>= (count @selected) constants/max-album-photos) - (rf/dispatch [:toasts/upsert - {:id :random-id - :icon :info - :icon-color colors/danger-50-opa-40 - :container-style {:top (when platform/ios? 20)} - :text (i18n/label :t/only-6-images)}]) - (swap! selected conj item)))) - :accessibility-label (str "image-" index)} - [rn/image - {:source {:uri (:uri item)} - :style (style/image window-width index)}] - (when (some #(= (:uri item) (:uri %)) @selected) - [rn/view {:style (style/overlay window-width)}]) - (when (some #(= (:uri item) (:uri %)) @selected) - [info-count/info-count - {:style style/image-count - :accessibility-label (str "count-" index)} - (inc (utils/first-index #(= (:uri item) (:uri %)) @selected))])]) - -(defn album-title - [photos? selected temporary-selected insets close] - (fn [] - (let [selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] - [rn/touchable-opacity - {:style (style/title-container) - :active-opacity 1 - :accessibility-label :album-title - :on-press (fn [] - ;; TODO: album-selector issue: - ;; https://github.com/status-im/status-mobile/issues/15398 - (if photos? - (do - (reset! temporary-selected @selected) - (rf/dispatch [:open-modal :album-selector {:insets insets}])) - (close)))} - [quo/text - {:weight :medium - :ellipsize-mode :tail - :number-of-lines 1 - :style {:max-width 150}} - selected-album] - [rn/view {:style (style/chevron-container)} - [quo/icon (if photos? :i/chevron-down :i/chevron-up) - {:color (colors/theme-colors colors/neutral-100 colors/white)}]]]))) + (let [item-selected? (some #(= (:uri item) (:uri %)) @selected)] + [rn/touchable-opacity + {:on-press (fn [] + (if item-selected? + (swap! selected remove-selected item) + (if (>= (count @selected) constants/max-album-photos) + (show-toast) + (swap! selected conj item)))) + :accessibility-label (str "image-" index)} + [rn/image + {:source {:uri (:uri item)} + :style (style/image window-width index)}] + (when item-selected? + [:<> + [rn/view {:style (style/overlay window-width)}] + [info-count/info-count + {:style style/image-count + :accessibility-label (str "count-" index)} + (inc (utils.collection/first-index #(= (:uri item) (:uri %)) @selected))]])])) (defn photo-selector - [{:keys [scroll-enabled on-scroll close]}] - [:f> - (let [temporary-selected (reagent/atom []) - {:keys [insets]} (rf/sub [:get-screen-params])] ; used when switching albums - (fn [] - (let [selected (reagent/atom []) ; currently selected - selected-images (rf/sub [:chats/sending-image]) - selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] - (rn/use-effect - (fn [] - (rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album]) - (if (seq selected-images) - (reset! selected (vec (vals selected-images))) - (reset! selected @temporary-selected))) - [selected-album]) - [:f> - (fn [] - (let [window-width (:width (rn/get-window)) - 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 {:flex 1}} - [rn/view - {:style style/buttons-container} - [album-title true selected temporary-selected insets close] - [clear-button selected]] - [gesture/flat-list - {:key-fn identity - :render-fn image - :render-data {:window-width window-width :selected selected} - :data camera-roll-photos - :num-columns 3 - :content-container-style {:width "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 insets selected]]))])))]) + [{:keys [scroll-enabled on-scroll close] :as sheet}] + (rf/dispatch [:photo-selector/get-photos-for-selected-album]) + (rf/dispatch [:photo-selector/camera-roll-get-albums]) + (let [album? (reagent/atom false) + sending-image (into [] (vals (rf/sub [:chats/sending-image]))) + selected-images (reagent/atom sending-image) + window-width (:width (rn/get-window))] + (fn [] + (let [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]) + selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] + [rn/view {:style {:flex 1 :margin-top -20}} + [rn/view {:style style/buttons-container} + [quo/dropdown {:type :grey :size 32 :on-change #(swap! album? not) :selected @album?} + selected-album] + [clear-button @album? selected-images]] + (if @album? + [album-selector/album-selector sheet album? selected-album] + [:<> + [gesture/flat-list + {:key-fn identity + :render-fn render-image + :render-data {:window-width window-width :selected selected-images} + :data camera-roll-photos + :num-columns 3 + :content-container-style {:width "100%" + :padding-bottom (+ (safe-area/get-bottom) 100) + :padding-top 64} + :on-scroll on-scroll + :scroll-enabled @scroll-enabled + :on-end-reached (fn [] + (when (and (not loading?) has-next-page?) + (rf/dispatch [:photo-selector/camera-roll-loading-more true]) + (rf/dispatch [:photo-selector/get-photos-for-selected-album + end-cursor])))}] + [confirm-button @selected-images sending-image close]])])))) diff --git a/src/status_im2/contexts/quo_preview/dropdowns/dropdown.cljs b/src/status_im2/contexts/quo_preview/dropdowns/dropdown.cljs index 0ad5eac70b..1c2eafc0b0 100644 --- a/src/status_im2/contexts/quo_preview/dropdowns/dropdown.cljs +++ b/src/status_im2/contexts/quo_preview/dropdowns/dropdown.cljs @@ -1,76 +1,78 @@ (ns status-im2.contexts.quo-preview.dropdowns.dropdown - (:require [quo2.components.dropdowns.dropdown :as quo2] - [quo2.foundations.colors :as colors] + (:require [quo2.foundations.colors :as colors] [react-native.core :as rn] [reagent.core :as reagent] - [status-im2.contexts.quo-preview.preview :as preview])) + [status-im2.contexts.quo-preview.preview :as preview] + [quo2.core :as quo])) (def descriptor - [{:label "Icon" - :key :icon + [{:label "Type:" + :key :type :type :select - :options [{:key :main-icons/placeholder - :value "Placeholder"} - {:key :main-icons/locked - :value "Wallet"}]} - {:label "Disabled" - :key :disabled? - :type :boolean} - {:label "Default item" - :key :default-item - :type :text} - {:label "Use border?" - :key :use-border? - :type :boolean} - {:label "Border color" - :key :border-color - :type :select - :options (map - (fn [c] - {:key c - :value c}) - (keys colors/customization))} - {:label "DD color" - :key :dd-color - :type :text} - {:label "Size" + :options [{:key :primary + :value "Primary"} + {:key :secondary + :value "Secondary"} + {:key :grey + :value "Grey"} + {:key :dark-grey + :value "Dark Grey"} + {:key :outline + :value "Outline"} + {:key :ghost + :value "Ghost"} + {:key :danger + :value "Danger"} + {:key :positive + :value "Positive"}]} + {:label "Size:" :key :size :type :select - :options [{:key :big - :value "big"} - {:key :medium - :value "medium"} - {:key :small - :value "small"}]}]) + :options [{:key 56 + :value "56"} + {:key 40 + :value "40"} + {:key 32 + :value "32"} + {:key 24 + :value "24"}]} + {:label "Icon:" + :key :icon + :type :boolean} + {:label "Before icon:" + :key :before + :type :boolean} + {:label "Disabled:" + :key :disabled + :type :boolean} + {:label "Label" + :key :label + :type :text}]) (defn cool-preview [] - (let [items ["Banana" - "Apple" - "COVID +18" - "Orange" - "Kryptonite" - "BMW" - "Meh"] - state (reagent/atom {:icon :main-icons/placeholder - :default-item "item1" - :use-border? false - :dd-color (colors/custom-color :purple 50) - :size :big}) - selected-item (reagent/cursor state [:default-item]) - on-select #(reset! selected-item %)] + (let [state (reagent/atom {:label "Press Me" + :size 40}) + label (reagent/cursor state [:label]) + before (reagent/cursor state [:before]) + icon (reagent/cursor state [:icon])] (fn [] [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} [rn/view {:padding-bottom 150} [preview/customizer state descriptor] [rn/view {:padding-vertical 60 - :align-items :center} - [rn/text (str "Selected item: " @selected-item)] - [quo2/dropdown - (merge @state - {:on-select on-select - :items items})]]]]))) + :flex-direction :row + :justify-content :center} + [quo/dropdown + (merge (dissoc @state + :theme + :before + :after) + {:on-press #(println "Hello world!")} + (when @before + {:before :i/placeholder})) + (if @icon :i/placeholder @label)]]]]))) (defn preview-dropdown [] diff --git a/src/status_im2/events.cljs b/src/status_im2/events.cljs index 42e3b62fca..25a590f61f 100644 --- a/src/status_im2/events.cljs +++ b/src/status_im2/events.cljs @@ -18,7 +18,8 @@ status-im2.contexts.syncing.events status-im2.contexts.chat.events status-im2.common.password-authentication.events - status-im2.contexts.communities.overview.events)) + status-im2.contexts.communities.overview.events + status-im2.contexts.chat.photo-selector.events)) (re-frame/reg-cofx :now diff --git a/src/status_im2/navigation/screens.cljs b/src/status_im2/navigation/screens.cljs index 6f34fe5667..28b48a2f9a 100644 --- a/src/status_im2/navigation/screens.cljs +++ b/src/status_im2/navigation/screens.cljs @@ -5,7 +5,6 @@ [status-im2.contexts.add-new-contact.views :as add-new-contact] [status-im2.contexts.chat.lightbox.view :as lightbox] [status-im2.contexts.chat.messages.view :as chat] - [status-im2.contexts.chat.photo-selector.album-selector.view :as album-selector] [status-im2.contexts.chat.photo-selector.view :as photo-selector] [status-im2.contexts.communities.discover.view :as communities.discover] [status-im2.contexts.communities.overview.view :as communities.overview] @@ -77,10 +76,6 @@ :options {:sheet? true} :component photo-selector/photo-selector} - {:name :album-selector - :options {:sheet? true} - :component album-selector/album-selector} - {:name :new-contact :options {:sheet? true} :component add-new-contact/new-contact}