improve photo-selector and adjust according to the latest designs (#16053)

This commit is contained in:
flexsurfer 2023-06-01 10:35:57 +02:00 committed by GitHub
parent 68e53e05bd
commit a4bc18ee3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 368 additions and 645 deletions

View File

@ -220,10 +220,10 @@
only icon only icon
[button {:icon true} :i/close-circle]" [button {:icon true} :i/close-circle]"
[_ _] [_ _]
(let [pressed (reagent/atom false)] (let [pressed-in (reagent/atom false)]
(fn (fn
[{:keys [on-press disabled type size before after above [{:keys [on-press disabled type size before after above icon-secondary-no-color
width customization-color override-theme override-background-color width customization-color override-theme override-background-color pressed
on-long-press accessibility-label icon icon-no-color style inner-style test-ID] on-long-press accessibility-label icon icon-no-color style inner-style test-ID]
:or {type :primary :or {type :primary
size 40 size 40
@ -234,17 +234,17 @@
[(or [(or
override-theme override-theme
(theme/get-theme)) type]) (theme/get-theme)) type])
state (cond disabled :disabled state (cond disabled :disabled
@pressed :pressed (or @pressed-in pressed) :pressed
:else :default) :else :default)
icon-size (when (= 24 size) 12) icon-size (when (= 24 size) 12)
icon-secondary-color (or icon-secondary-color icon-color)] icon-secondary-color (or icon-secondary-color icon-color)]
[rn/touchable-without-feedback [rn/touchable-without-feedback
(merge {:test-ID test-ID (merge {:test-ID test-ID
:disabled disabled :disabled disabled
:accessibility-label accessibility-label :accessibility-label accessibility-label
:on-press-in #(reset! pressed true) :on-press-in #(reset! pressed-in true)
:on-press-out #(reset! pressed nil)} :on-press-out #(reset! pressed-in nil)}
(when on-press (when on-press
{:on-press on-press}) {:on-press on-press})
(when on-long-press (when on-long-press
@ -306,5 +306,6 @@
[quo2.icons/icon after [quo2.icons/icon after
{:container-style {:margin-left 4 {:container-style {:margin-left 4
:margin-right (if (= size 40) 12 8)} :margin-right (if (= size 40) 12 8)}
:no-color icon-secondary-no-color
:color icon-secondary-color :color icon-secondary-color
:size icon-size}]])]]])))) :size icon-size}]])]]]))))

View File

@ -1,165 +1,14 @@
(ns quo2.components.dropdowns.dropdown (ns quo2.components.dropdowns.dropdown
(:require [quo2.components.icon :as icons] (:require [quo2.components.buttons.button :as button]))
[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}]]]))
(defn dropdown (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]))

View File

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

View File

@ -17,6 +17,7 @@
(def view (reagent/adapt-react-class (.-View ^js react-native))) (def view (reagent/adapt-react-class (.-View ^js react-native)))
(def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native))) (def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native)))
(def image (reagent/adapt-react-class (.-Image ^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 (reagent/adapt-react-class (.-Text ^js react-native)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js react-native))) (def text-input (reagent/adapt-react-class (.-TextInput ^js react-native)))

View File

@ -91,7 +91,7 @@
[flat-list [flat-list
(merge props (merge props
{:data data {:data data
:render-fn (fn [item] :render-fn (fn [p1 p2 p3 p4]
(if (:header? item) (if (:header? p1)
(render-section-header-fn item) [render-section-header-fn p1 p2 p3 p4]
(render-fn item)))})])) [render-fn p1 p2 p3 p4]))})]))

View File

@ -1,47 +1,17 @@
(ns status-im.chat.models.images (ns status-im.chat.models.images
(:require ["@react-native-community/cameraroll" :as CameraRoll] (:require ["@react-native-community/cameraroll" :as CameraRoll]
["react-native-blob-util" :default ReactNativeBlobUtil] ["react-native-blob-util" :default ReactNativeBlobUtil]
[clojure.string :as string]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[status-im.ui.components.permissions :as permissions] [react-native.permissions :as permissions]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im2.config :as config] [status-im2.config :as config]
[status-im.utils.fs :as fs] [status-im.utils.fs :as fs]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[status-im.utils.image-processing :as image-processing]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.utils.types :as types]
[status-im.utils.utils :as utils] [status-im.utils.utils :as utils]
[taoensso.timbre :as log])) [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")) (def temp-image-url (str (fs/cache-dir) "/StatusIm_Image.jpeg"))
(defn download-image-http (defn download-image-http
@ -78,108 +48,6 @@
#(re-frame/dispatch [:chat.ui/image-captured current-chat-id (.-path %)]) #(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 (rf/defn image-captured
{:events [:chat.ui/image-captured]} {:events [:chat.ui/image-captured]}
[{:keys [db]} chat-id uri] [{:keys [db]} chat-id uri]
@ -189,81 +57,17 @@
(not (get images uri))) (not (get images uri)))
{::image-selected [uri current-chat-id]}))) {::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 (rf/defn clear-sending-images
{:events [:chat.ui/clear-sending-images]} {:events [:chat.ui/clear-sending-images]}
[{:keys [db]}] [{:keys [db]}]
{:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})}) {: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 (rf/defn image-unselected
{:events [:chat.ui/image-unselected]} {:events [:chat.ui/image-unselected]}
[{:keys [db]} original] [{:keys [db]} original]
(let [current-chat-id (:current-chat-id db)] (let [current-chat-id (:current-chat-id db)]
{:db (update-in db [:chat/inputs current-chat-id :metadata :sending-image] dissoc (:uri original))})) {: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 (rf/defn chat-show-image-picker-camera
{:events [:chat.ui/show-image-picker-camera]} {:events [:chat.ui/show-image-picker-camera]}
[{:keys [db]} chat-id] [{:keys [db]} chat-id]
@ -271,25 +75,3 @@
images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])] images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])]
(when (< (count images) config/max-images-batch) (when (< (count images) config/max-images-batch)
{::chat-open-image-picker-camera current-chat-id}))) {::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})

View File

@ -36,7 +36,7 @@
status-im.signals.core status-im.signals.core
status-im.stickers.core status-im.stickers.core
status-im.transport.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.components.react :as react]
status-im.ui.screens.privacy-and-security-settings.events status-im.ui.screens.privacy-and-security-settings.events
[status-im.utils.dimensions :as dimensions] [status-im.utils.dimensions :as dimensions]

View File

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

View File

@ -11,7 +11,7 @@
[status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.components.chat-icon.screen :as chat-icon]
[status-im.ui.components.connectivity.view :as connectivity] [status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.icons.icons :as icons] [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.react :as react]
[status-im.ui.components.tooltip.views :as tooltip] [status-im.ui.components.tooltip.views :as tooltip]
[status-im.ui.components.webview :as components.webview] [status-im.ui.components.webview :as components.webview]

View File

@ -1,7 +1,6 @@
(ns status-im2.contexts.chat.events-test (ns status-im2.contexts.chat.events-test
(:require [cljs.test :refer-macros [deftest is testing]] (:require [cljs.test :refer-macros [deftest is testing]]
[status-im2.contexts.chat.events :as chat] [status-im2.contexts.chat.events :as chat]
[status-im.chat.models.images :as images]
[status-im.utils.clocks :as utils.clocks])) [status-im.utils.clocks :as utils.clocks]))
(deftest clear-history-test (deftest clear-history-test
@ -90,8 +89,3 @@
(testing "Pagination info should be reset on navigation" (testing "Pagination info should be reset on navigation"
(let [res (chat/navigate-to-chat {:db db} chat-id)] (let [res (chat/navigate-to-chat {:db db} chat-id)]
(is (nil? (get-in res [:db :pagination-info chat-id :all-loaded?]))))))) (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

@ -6,16 +6,16 @@
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[quo2.foundations.colors :as colors] [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])) [status-im2.contexts.chat.photo-selector.album-selector.style :as style]))
(defn album (defn render-album
[{:keys [title count uri]} index _ selected-album] [{:keys [title count uri]} index _ {:keys [album? selected-album]}]
(let [selected? (= selected-album title)] (let [selected? (= selected-album title)]
[rn/touchable-opacity [rn/touchable-opacity
{:on-press (fn [] {:on-press (fn []
(rf/dispatch [:chat.ui/camera-roll-select-album title]) (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?) :style (style/album-container selected?)
:accessibility-label (str "album-" index)} :accessibility-label (str "album-" index)}
[rn/image [rn/image
@ -31,7 +31,7 @@
[quo/text [quo/text
{:size :paragraph-2 {:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}} :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? (when selected?
[rn/view [rn/view
{:style {:position :absolute {:style {:position :absolute
@ -39,9 +39,11 @@
[quo/icon :i/check [quo/icon :i/check
{:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60)}]])])) {:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60)}]])]))
(def no-title "no-title")
(defn section-header (defn section-header
[{:keys [title]}] [{:keys [title]}]
(when (not= title "smart-albums") (when-not (= title no-title)
[quo/divider-label [quo/divider-label
{:label title {:label title
:container-style style/divider}])) :container-style style/divider}]))
@ -51,22 +53,19 @@
(str (:title item) index)) (str (:title item) index))
(defn album-selector (defn album-selector
[{:keys [on-scroll]}] [{:keys [scroll-enabled on-scroll]} album? selected-album]
(rf/dispatch [:chat.ui/camera-roll-get-albums]) (let [albums (rf/sub [:camera-roll/albums])
(fn [{:keys [scroll-enabled]}] albums-sections [{:title no-title :data (:smart-albums albums)}
(let [albums (rf/sub [:camera-roll/albums]) {:title (i18n/label :t/my-albums) :data (:my-albums albums)}]]
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] [gesture/section-list
[rn/view {:style {:padding-top 20}} {:data albums-sections
[album-title false] :sections albums-sections
[gesture/section-list :render-data {:album? album? :selected-album selected-album}
{:data albums :render-fn render-album
:render-fn album :sticky-section-headers-enabled false
:render-data selected-album :render-section-header-fn section-header
:sections albums :content-container-style {:padding-top 64
:sticky-section-headers-enabled false :padding-bottom 40}
:render-section-header-fn section-header :key-fn key-fn
:style {:margin-top 12} :scroll-enabled @scroll-enabled
:content-container-style {:padding-bottom 40} :on-scroll on-scroll}]))
:key-fn key-fn
:scroll-enabled @scroll-enabled
:on-scroll on-scroll}]])))

View File

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

View File

@ -15,18 +15,13 @@
:flex-direction :row :flex-direction :row
:left 0 :left 0
:right 0 :right 0
:top 0 :top 20
:justify-content :center :justify-content :center
:z-index 1}) :z-index 1})
(defn clear-container (def clear-container
[] {:position :absolute
{:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80) :right 20})
:padding-horizontal 12
:padding-vertical 5
:border-radius 10
:position :absolute
:right 20})
(defn close-button-container (defn close-button-container
[] []

View File

@ -1,150 +1,126 @@
(ns status-im2.contexts.chat.photo-selector.view (ns status-im2.contexts.chat.photo-selector.view
(:require (:require
[react-native.gesture :as gesture] [react-native.gesture :as gesture]
[react-native.safe-area :as safe-area]
[react-native.platform :as platform] [react-native.platform :as platform]
[status-im2.constants :as constants]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf]
[quo2.components.notifications.info-count :as info-count] [quo2.components.notifications.info-count :as info-count]
[quo2.core :as quo] [quo2.core :as quo]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient] [react-native.linear-gradient :as linear-gradient]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im2.constants :as constants]
[status-im2.contexts.chat.photo-selector.style :as style] [status-im2.contexts.chat.photo-selector.style :as style]
[status-im.utils.core :as utils] [status-im2.contexts.chat.photo-selector.album-selector.view :as album-selector]
[quo.react] utils.collection))
[utils.re-frame :as rf]))
(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 (defn on-press-confirm-selection
[selected] [selected close]
(rf/dispatch [:chat.ui/clear-sending-images]) (rf/dispatch [:chat.ui/clear-sending-images])
(doseq [item @selected] (doseq [item selected]
(rf/dispatch [:chat.ui/camera-roll-pick item])) (rf/dispatch [:photo-selector/camera-roll-pick item]))
(reset! selected []) (close))
(rf/dispatch [:navigate-back]))
(defn bottom-gradient (defn confirm-button
[selected-images insets selected] [selected-images sending-image close]
(when (or (seq @selected) (seq selected-images)) (when (not= selected-images sending-image)
[linear-gradient/linear-gradient [linear-gradient/linear-gradient
{:colors [:black :transparent] {:colors [:black :transparent]
:start {:x 0 :y 1} :start {:x 0 :y 1}
:end {:x 0 :y 0} :end {:x 0 :y 0}
:style (style/gradient-container (:bottom insets))} :style (style/gradient-container (safe-area/get-bottom))}
[quo/button [quo/button
{:style {:align-self :stretch {:style {:align-self :stretch
:margin-horizontal 20 :margin-horizontal 20
:margin-top 12} :margin-top 12}
:on-press #(on-press-confirm-selection selected) :on-press #(on-press-confirm-selection selected-images close)
:accessibility-label :confirm-selection} :accessibility-label :confirm-selection}
(i18n/label :t/confirm-selection)]])) (i18n/label :t/confirm-selection)]]))
(defn clear-button (defn clear-button
[selected] [album? selected]
(when (seq @selected) (when (and (not album?) (seq @selected))
[rn/touchable-opacity [rn/view {:style style/clear-container}
{:on-press #(reset! selected []) [quo/button
:style (style/clear-container) {:type :grey
:accessibility-label :clear} :size 32
[quo/text {:weight :medium} (i18n/label :t/clear)]])) :accessibility-label :clear
:on-press #(reset! selected [])}
(i18n/label :t/clear)]]))
(defn remove-selected (defn remove-selected
[coll item] [coll item]
(vec (remove #(= (:uri item) (:uri %)) coll))) (vec (remove #(= (:uri item) (:uri %)) coll)))
(defn image (defn render-image
[item index _ {:keys [window-width selected]}] [item index _ {:keys [window-width selected]}]
[rn/touchable-opacity (let [item-selected? (some #(= (:uri item) (:uri %)) @selected)]
{:active-opacity 1 [rn/touchable-opacity
:on-press (fn [] {:on-press (fn []
(if (some #(= (:uri item) (:uri %)) @selected) (if item-selected?
(swap! selected remove-selected item) (swap! selected remove-selected item)
(if (>= (count @selected) constants/max-album-photos) (if (>= (count @selected) constants/max-album-photos)
(rf/dispatch [:toasts/upsert (show-toast)
{:id :random-id (swap! selected conj item))))
:icon :info :accessibility-label (str "image-" index)}
:icon-color colors/danger-50-opa-40 [rn/image
:container-style {:top (when platform/ios? 20)} {:source {:uri (:uri item)}
:text (i18n/label :t/only-6-images)}]) :style (style/image window-width index)}]
(swap! selected conj item)))) (when item-selected?
:accessibility-label (str "image-" index)} [:<>
[rn/image [rn/view {:style (style/overlay window-width)}]
{:source {:uri (:uri item)} [info-count/info-count
:style (style/image window-width index)}] {:style style/image-count
(when (some #(= (:uri item) (:uri %)) @selected) :accessibility-label (str "count-" index)}
[rn/view {:style (style/overlay window-width)}]) (inc (utils.collection/first-index #(= (:uri item) (:uri %)) @selected))]])]))
(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)}]]])))
(defn photo-selector (defn photo-selector
[{:keys [scroll-enabled on-scroll close]}] [{:keys [scroll-enabled on-scroll close] :as sheet}]
[:f> (rf/dispatch [:photo-selector/get-photos-for-selected-album])
(let [temporary-selected (reagent/atom []) (rf/dispatch [:photo-selector/camera-roll-get-albums])
{:keys [insets]} (rf/sub [:get-screen-params])] ; used when switching albums (let [album? (reagent/atom false)
(fn [] sending-image (into [] (vals (rf/sub [:chats/sending-image])))
(let [selected (reagent/atom []) ; currently selected selected-images (reagent/atom sending-image)
selected-images (rf/sub [:chats/sending-image]) window-width (:width (rn/get-window))]
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))] (fn []
(rn/use-effect (let [camera-roll-photos (rf/sub [:camera-roll/photos])
(fn [] end-cursor (rf/sub [:camera-roll/end-cursor])
(rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album]) loading? (rf/sub [:camera-roll/loading-more])
(if (seq selected-images) has-next-page? (rf/sub [:camera-roll/has-next-page])
(reset! selected (vec (vals selected-images))) selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
(reset! selected @temporary-selected))) [rn/view {:style {:flex 1 :margin-top -20}}
[selected-album]) [rn/view {:style style/buttons-container}
[:f> [quo/dropdown {:type :grey :size 32 :on-change #(swap! album? not) :selected @album?}
(fn [] selected-album]
(let [window-width (:width (rn/get-window)) [clear-button @album? selected-images]]
camera-roll-photos (rf/sub [:camera-roll/photos]) (if @album?
end-cursor (rf/sub [:camera-roll/end-cursor]) [album-selector/album-selector sheet album? selected-album]
loading? (rf/sub [:camera-roll/loading-more]) [:<>
has-next-page? (rf/sub [:camera-roll/has-next-page])] [gesture/flat-list
[rn/view {:style {:flex 1}} {:key-fn identity
[rn/view :render-fn render-image
{:style style/buttons-container} :render-data {:window-width window-width :selected selected-images}
[album-title true selected temporary-selected insets close] :data camera-roll-photos
[clear-button selected]] :num-columns 3
[gesture/flat-list :content-container-style {:width "100%"
{:key-fn identity :padding-bottom (+ (safe-area/get-bottom) 100)
:render-fn image :padding-top 64}
:render-data {:window-width window-width :selected selected} :on-scroll on-scroll
:data camera-roll-photos :scroll-enabled @scroll-enabled
:num-columns 3 :on-end-reached (fn []
:content-container-style {:width "100%" (when (and (not loading?) has-next-page?)
:padding-bottom (+ (:bottom insets) 100) (rf/dispatch [:photo-selector/camera-roll-loading-more true])
:padding-top 64} (rf/dispatch [:photo-selector/get-photos-for-selected-album
:on-scroll on-scroll end-cursor])))}]
:scroll-enabled @scroll-enabled [confirm-button @selected-images sending-image close]])]))))
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
selected-album loading?
has-next-page?])}]
[bottom-gradient selected-images insets selected]]))])))])

View File

@ -1,76 +1,78 @@
(ns status-im2.contexts.quo-preview.dropdowns.dropdown (ns status-im2.contexts.quo-preview.dropdowns.dropdown
(:require [quo2.components.dropdowns.dropdown :as quo2] (:require [quo2.foundations.colors :as colors]
[quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[reagent.core :as reagent] [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 (def descriptor
[{:label "Icon" [{:label "Type:"
:key :icon :key :type
:type :select :type :select
:options [{:key :main-icons/placeholder :options [{:key :primary
:value "Placeholder"} :value "Primary"}
{:key :main-icons/locked {:key :secondary
:value "Wallet"}]} :value "Secondary"}
{:label "Disabled" {:key :grey
:key :disabled? :value "Grey"}
:type :boolean} {:key :dark-grey
{:label "Default item" :value "Dark Grey"}
:key :default-item {:key :outline
:type :text} :value "Outline"}
{:label "Use border?" {:key :ghost
:key :use-border? :value "Ghost"}
:type :boolean} {:key :danger
{:label "Border color" :value "Danger"}
:key :border-color {:key :positive
:type :select :value "Positive"}]}
:options (map {:label "Size:"
(fn [c]
{:key c
:value c})
(keys colors/customization))}
{:label "DD color"
:key :dd-color
:type :text}
{:label "Size"
:key :size :key :size
:type :select :type :select
:options [{:key :big :options [{:key 56
:value "big"} :value "56"}
{:key :medium {:key 40
:value "medium"} :value "40"}
{:key :small {:key 32
:value "small"}]}]) :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 (defn cool-preview
[] []
(let [items ["Banana" (let [state (reagent/atom {:label "Press Me"
"Apple" :size 40})
"COVID +18" label (reagent/cursor state [:label])
"Orange" before (reagent/cursor state [:before])
"Kryptonite" icon (reagent/cursor state [:icon])]
"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 %)]
(fn [] (fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150} [rn/view {:padding-bottom 150}
[preview/customizer state descriptor] [preview/customizer state descriptor]
[rn/view [rn/view
{:padding-vertical 60 {:padding-vertical 60
:align-items :center} :flex-direction :row
[rn/text (str "Selected item: " @selected-item)] :justify-content :center}
[quo2/dropdown [quo/dropdown
(merge @state (merge (dissoc @state
{:on-select on-select :theme
:items items})]]]]))) :before
:after)
{:on-press #(println "Hello world!")}
(when @before
{:before :i/placeholder}))
(if @icon :i/placeholder @label)]]]])))
(defn preview-dropdown (defn preview-dropdown
[] []

View File

@ -18,7 +18,8 @@
status-im2.contexts.syncing.events status-im2.contexts.syncing.events
status-im2.contexts.chat.events status-im2.contexts.chat.events
status-im2.common.password-authentication.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 (re-frame/reg-cofx
:now :now

View File

@ -5,7 +5,6 @@
[status-im2.contexts.add-new-contact.views :as add-new-contact] [status-im2.contexts.add-new-contact.views :as add-new-contact]
[status-im2.contexts.chat.lightbox.view :as lightbox] [status-im2.contexts.chat.lightbox.view :as lightbox]
[status-im2.contexts.chat.messages.view :as chat] [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.chat.photo-selector.view :as photo-selector]
[status-im2.contexts.communities.discover.view :as communities.discover] [status-im2.contexts.communities.discover.view :as communities.discover]
[status-im2.contexts.communities.overview.view :as communities.overview] [status-im2.contexts.communities.overview.view :as communities.overview]
@ -77,10 +76,6 @@
:options {:sheet? true} :options {:sheet? true}
:component photo-selector/photo-selector} :component photo-selector/photo-selector}
{:name :album-selector
:options {:sheet? true}
:component album-selector/album-selector}
{:name :new-contact {:name :new-contact
:options {:sheet? true} :options {:sheet? true}
:component add-new-contact/new-contact} :component add-new-contact/new-contact}