diff --git a/components/src/status_im/ui/components/react.cljs b/components/src/status_im/ui/components/react.cljs index 98a8b81a37..83b6fef210 100644 --- a/components/src/status_im/ui/components/react.cljs +++ b/components/src/status_im/ui/components/react.cljs @@ -165,12 +165,15 @@ (utils/show-popup (i18n/label :t/error) (i18n/label :t/photos-access-error)))) -(defn show-image-picker [images-fn] - (let [image-picker (.-default image-picker-class)] - (-> image-picker - (.openPicker (clj->js {:multiple false})) - (.then images-fn) - (.catch show-access-error)))) +(defn show-image-picker + ([images-fn] + (show-image-picker images-fn nil)) + ([images-fn media-type] + (let [image-picker (.-default image-picker-class)] + (-> image-picker + (.openPicker (clj->js {:multiple false :mediaType (or media-type "any")})) + (.then images-fn) + (.catch show-access-error))))) ;; Clipboard diff --git a/src/status_im/extensions/camera.cljs b/src/status_im/extensions/camera.cljs new file mode 100644 index 0000000000..db8430ecf8 --- /dev/null +++ b/src/status_im/extensions/camera.cljs @@ -0,0 +1,63 @@ +(ns status-im.extensions.camera + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] + [status-im.utils.fx :as fx] + [status-im.extensions.views :as ext-views] + [status-im.ui.components.list-selection :as list-selection] + [status-im.qr-scanner.core :as qr-scanner] + [status-im.ui.screens.navigation :as navigation] + [status-im.utils.handlers :as handlers])) + +;; Common + +(handlers/register-handler-fx + :extensions/camera-cancel + (fn [_ [_ _ {{:keys [on-failure]} :data}]] + (when on-failure + (re-frame/dispatch (on-failure {:result "user cancelled"}))))) + +(handlers/register-handler-fx + :extensions/camera-denied + (fn [_ [_ {{:keys [on-failure]} :data}]] + (when on-failure + (re-frame/dispatch (on-failure {:result "user denied access to camera"}))))) + +;; Photo taker\picker + +(handlers/register-handler-fx + :extensions/camera-error + (fn [cofx [_ error {:keys [on-failure]}]] + (when on-failure + {:dispatch (on-failure {:result error})}))) + +(handlers/register-handler-fx + :extensions/camera-picture-taken + (fn [cofx [_ base64 {{:keys [on-success]} :data}]] + (fx/merge cofx + {:dispatch (on-success {:result base64})} + (navigation/navigate-back)))) + +(handlers/register-handler-fx + :extensions/camera-picture + (fn [_ [_ _ params]] + (list-selection/show (ext-views/pick-or-take-picture-list-selection {:data params})) + {})) + +;; QR code scanner + +(handlers/register-handler-fx + :extensions/camera-qr-code-scanned + (fn [cofx [_ _ qr-code {{:keys [on-success]} :data}]] + (fx/merge cofx + {:dispatch (on-success {:result qr-code})} + (navigation/navigate-back)))) + +(handlers/register-handler-fx + :extensions/camera-qr-code + (fn [{:keys [db] :as cofx} [_ _ {:keys [on-success on-failure]}]] + (qr-scanner/scan-qr-code cofx {:modal? false + :deny-handler :extensions/camera-denied} + {:handler :extensions/camera-qr-code-scanned + :cancel-handler :extensions/camera-cancel + :data {:on-success on-success + :on-failure on-failure}}))) \ No newline at end of file diff --git a/src/status_im/extensions/core.cljs b/src/status_im/extensions/core.cljs index ceb522d136..5ce12a35b9 100644 --- a/src/status_im/extensions/core.cljs +++ b/src/status_im/extensions/core.cljs @@ -20,6 +20,7 @@ [status-im.utils.handlers :as handlers] [status-im.utils.fx :as fx] status-im.extensions.ethereum + status-im.extensions.camera [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.ethereum.core :as ethereum] [status-im.chat.commands.sending :as commands-sending])) @@ -464,6 +465,16 @@ :arguments {:values :vector :operation {:one-of #{:plus :minus :times :divide}} :on-result :event}} + 'camera/picture + {:permissions [:read] + :value :extensions/camera-picture + :arguments {:on-success :event + :on-failure? :event}} + 'camera/qr-code + {:permissions [:read] + :value :extensions/camera-qr-code + :arguments {:on-success :event + :on-failure? :event}} 'schedule/start {:permissions [:read] :value :extensions/schedule-start diff --git a/src/status_im/extensions/views.cljs b/src/status_im/extensions/views.cljs new file mode 100644 index 0000000000..7a56701b2b --- /dev/null +++ b/src/status_im/extensions/views.cljs @@ -0,0 +1,69 @@ +(ns status-im.extensions.views + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] + [status-im.ui.components.camera :as camera] + [status-im.ui.components.react :as react] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.toolbar.actions :as actions] + [status-im.ui.components.icons.vector-icons :as icons] + [status-im.i18n :as i18n] + [status-im.ui.screens.profile.photo-capture.styles :as styles] + [status-im.utils.image-processing :as image-processing])) + +;; ensure photo taken or picked are not too big +(def max-width 1024) +(def max-height 1024) + +(defn- process-image-and-finish [path context] + (let [on-success (fn [base64] + (re-frame/dispatch [:extensions/camera-picture-taken base64 context])) + on-error (fn [type error] + (re-frame/dispatch [:extensions/camera-error error context]))] + (image-processing/img->base64 path on-success on-error max-width max-height))) + +(defview take-picture [] + (letsubs [context [:get-screen-params] + camera-ref (reagent/atom false)] + [react/view styles/container + [status-bar/status-bar] + [toolbar/toolbar nil + [toolbar/nav-button (actions/back + #(do + (re-frame/dispatch [:extensions/camera-cancel context]) + (re-frame/dispatch [:navigate-back])))] + [toolbar/content-title (i18n/label :t/extensions-camera-send-picture)]] + [camera/camera {:style {:flex 1} + :aspect (:fill camera/aspects) + :captureTarget (:disk camera/capture-targets) + :ref #(reset! camera-ref %)}] + [react/view styles/button-container + [react/view styles/button + [react/touchable-highlight {:on-press (fn [] + (let [camera @camera-ref] + (-> (.capture camera) + (.then #(process-image-and-finish (.-path %) context)) + (.catch #(re-frame/dispatch [:extensions/camera-error % context])))))} + [react/view + [icons/icon :main-icons/camera {:color :white}]]]]]])) + +;TODO image picker doesn't notify when the user cancel image selection +; in this case we cannot notify cancel to extension +(defn- open-image-picker [context] + (react/show-image-picker + (fn [image] + (let [path (get (js->clj image) "path")] + (process-image-and-finish path context))) + "photo")) + +(defn pick-or-take-picture-list-selection [context] + {:title (i18n/label :t/extensions-camera-send-picture) + :options [{:label (i18n/label :t/image-source-gallery) + :action #(open-image-picker context)} + {:label (i18n/label :t/image-source-make-photo) + :action (fn [] + (re-frame/dispatch [:request-permissions {:permissions [:camera :write-external-storage] + :on-allowed #(re-frame/dispatch [:navigate-to :take-picture context]) + :on-denied #(re-frame/dispatch [:extensions/camera-denied context])}]))}] + :on-cancel #(re-frame/dispatch [:extensions/camera-cancel context])}) \ No newline at end of file diff --git a/src/status_im/qr_scanner/core.cljs b/src/status_im/qr_scanner/core.cljs index ffd7b0852b..28e5edee7a 100644 --- a/src/status_im/qr_scanner/core.cljs +++ b/src/status_im/qr_scanner/core.cljs @@ -5,16 +5,18 @@ [status-im.utils.fx :as fx])) (fx/defn scan-qr-code - [{:keys [db]} {:keys [modal?] :as identifier} qr-codes] + [{:keys [db]} {:keys [modal? deny-handler] :as identifier} qr-codes] {:db (assoc db :qr-codes qr-codes) :request-permissions-fx {:permissions [:camera] :on-allowed #(re-frame/dispatch [(if modal? :navigate-to-modal :navigate-to) :qr-scanner {:current-qr-context identifier}]) - :on-denied (fn [] - (utils/set-timeout - #(utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error)) - 50))}}) + :on-denied (if (nil? deny-handler) + (fn [] + (utils/set-timeout + #(utils/show-popup (i18n/label :t/error) + (i18n/label :t/camera-access-error)) + 50)) + #(re-frame/dispatch [deny-handler qr-codes]))}}) (fx/defn set-qr-code [{:keys [db]} context data] diff --git a/src/status_im/ui/components/action_sheet.cljs b/src/status_im/ui/components/action_sheet.cljs index 1a1dd31d25..7a0dacde28 100644 --- a/src/status_im/ui/components/action_sheet.cljs +++ b/src/status_im/ui/components/action_sheet.cljs @@ -3,11 +3,13 @@ [status-im.utils.core :as utils] [status-im.react-native.js-dependencies :as js-dependencies])) -(defn- callback [options] +(defn- callback [options on-cancel] (fn [index] - (when (< index (count options)) + (if (< index (count options)) (when-let [handler (:action (nth options index))] - (handler))))) + (handler)) + (when on-cancel + (on-cancel))))) (defn- prepare-options [title message options] (let [destructive-opt-index (utils/first-index :destructive? options)] ;; TODO Can only be a single destructive? @@ -18,7 +20,7 @@ (when title {:title title}) (when message {:message message}))))) -(defn show [{:keys [title message options]}] +(defn show [{:keys [title message options on-cancel]}] (.showActionSheetWithOptions (.-ActionSheetIOS js-dependencies/react-native) (prepare-options title message options) - (callback options))) + (callback options on-cancel))) diff --git a/src/status_im/ui/components/dialog.cljs b/src/status_im/ui/components/dialog.cljs index 74a5490c99..2d16845153 100644 --- a/src/status_im/ui/components/dialog.cljs +++ b/src/status_im/ui/components/dialog.cljs @@ -3,17 +3,21 @@ (def dialogs (.-default rn-dependencies/dialogs)) -(defn show [{:keys [title options cancel-text]}] +(defn show [{:keys [title options cancel-text on-cancel]}] (.. dialogs (showPicker title nil (clj->js {:items (mapv #(select-keys % [:label]) options) :negativeText cancel-text :positiveText nil})) (then (fn [selected] - (when-let [label (get-in (js->clj selected :keywordize-keys true) - [:selectedItem :label])] - (when-let [action (->> options - (filter #(= label (:label %))) - first - :action)] - (action))))))) + (let [result (js->clj selected :keywordize-keys true)] + (if (not= (get result :action) "actionSelect") + (when on-cancel + (on-cancel)) + (when-let [label (get-in result + [:selectedItem :label])] + (when-let [action (->> options + (filter #(= label (:label %))) + first + :action)] + (action))))))))) \ No newline at end of file diff --git a/src/status_im/ui/screens/profile/events.cljs b/src/status_im/ui/screens/profile/events.cljs index 220ea54af4..23b7d2ab8d 100644 --- a/src/status_im/ui/screens/profile/events.cljs +++ b/src/status_im/ui/screens/profile/events.cljs @@ -5,7 +5,6 @@ [status-im.ui.screens.profile.models :as profile.models] [status-im.ui.screens.profile.navigation] [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.screens.profile.models :as profile.models] [status-im.utils.handlers :as handlers] [status-im.utils.identicon :as identicon] [status-im.utils.universal-links.core :as universal-links])) diff --git a/src/status_im/ui/screens/profile/models.cljs b/src/status_im/ui/screens/profile/models.cljs index a9491c6ba2..53c10df43d 100644 --- a/src/status_im/ui/screens/profile/models.cljs +++ b/src/status_im/ui/screens/profile/models.cljs @@ -19,7 +19,8 @@ (re-frame/dispatch [callback-event base64])) on-error (fn [type error] (.log js/console type error))] - (image-processing/img->base64 path on-success on-error))))) + (image-processing/img->base64 path on-success on-error 150 150))) + "photo")) (defn send-transaction [chat-id {:keys [db] :as cofx}] (let [send-command (get-in db [:id->command ["send" #{:personal-chats}]])] diff --git a/src/status_im/ui/screens/profile/photo_capture/views.cljs b/src/status_im/ui/screens/profile/photo_capture/views.cljs index 1833504941..f173210fd8 100644 --- a/src/status_im/ui/screens/profile/photo_capture/views.cljs +++ b/src/status_im/ui/screens/profile/photo_capture/views.cljs @@ -20,7 +20,7 @@ (re-frame/dispatch [:navigate-back])) on-error (fn [type error] (log/debug type error))] - (image-processing/img->base64 path on-success on-error))) + (image-processing/img->base64 path on-success on-error 150 150))) (defn profile-photo-capture [] (let [camera-ref (reagent/atom nil)] diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index 80fda616bc..1fe50d9d2f 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -28,6 +28,7 @@ [status-im.ui.screens.profile.contact.views :as profile.contact] [status-im.ui.screens.profile.group-chat.views :as profile.group-chat] [status-im.ui.screens.profile.photo-capture.views :refer [profile-photo-capture]] + [status-im.extensions.views :refer [take-picture]] [status-im.ui.screens.wallet.main.views :as wallet.main] [status-im.ui.screens.wallet.collectibles.views :refer [collectibles-list]] [status-im.ui.screens.wallet.send.views :refer [send-transaction send-transaction-modal sign-message-modal]] @@ -169,6 +170,7 @@ :new add-new :new-chat new-chat :qr-scanner qr-scanner + :take-picture take-picture :new-group new-group :add-participants-toggle-list add-participants-toggle-list :contact-toggle-list contact-toggle-list diff --git a/src/status_im/utils/image_processing.cljs b/src/status_im/utils/image_processing.cljs index aa60b4b923..bc04491afe 100644 --- a/src/status_im/utils/image_processing.cljs +++ b/src/status_im/utils/image_processing.cljs @@ -18,7 +18,7 @@ (on-error :base64 error))] (read-file path "base64" on-success on-error))) -(defn img->base64 [path on-success on-error] +(defn img->base64 [path on-success on-error max-width max-height] (let [on-resized (fn [image] (let [path (object/get image "path")] (log/debug "Resized: " path) @@ -26,4 +26,4 @@ on-error (fn [error] (log/debug "Resized error: " error) (on-error :resize error))] - (resize path 150 150 on-resized on-error))) + (resize path max-width max-height on-resized on-error))) diff --git a/translations/en.json b/translations/en.json index cfed6cb5da..a03c0f57b6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -920,6 +920,7 @@ "mailserver-error-content": "The mailserver you selected couldn't be reached.", "dapps-permissions": "Dapps permissions", "revoke-access": "Revoke access", + "extensions-camera-send-picture": "Send picture", "mobile-syncing-sheet-title": "Sync using the mobile network", "mobile-syncing-sheet-details": "Status tends to use a lot of data when syncing chats. You can choose not to sync when on mobile network", "mobile-network-continue-syncing": "Continue syncing",