Add pick multiple images

Add inner border to image message

Add max batch size of 5

Disable camera roll more images

Use interop instead of aget

Add border as a separate layer

Signed-off-by: Gheorghe Pinzaru <feross95@gmail.com>
This commit is contained in:
Gheorghe Pinzaru 2020-10-19 16:02:19 +03:00
parent fe5b5ab4bc
commit cf4bef8268
No known key found for this signature in database
GPG Key ID: C9A094959935A952
13 changed files with 212 additions and 112 deletions

View File

@ -259,8 +259,8 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webview (10.3.1):
- React
- react-native-webview (10.9.2):
- React-Core
- React-RCTActionSheet (0.62.2):
- React-Core/RCTActionSheetHeaders (= 0.62.2)
- React-RCTAnimation (0.62.2):
@ -630,7 +630,7 @@ SPEC CHECKSUMS:
react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c
react-native-slider: 12bd76d3d568c9c5500825db54123d44b48e4ad4
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webview: 40bbeb6d011226f34cb83f845aeb0fdf515cfc5f
react-native-webview: 4e96d493f9f90ba4f03b28933f30b2964df07e39
React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c
React-RCTAnimation: 49ab98b1c1ff4445148b72a3d61554138565bad0
React-RCTBlob: a332773f0ebc413a0ce85942a55b064471587a71

View File

@ -46,7 +46,8 @@
:shadow-01 "rgba(0,9,26,0.12)" ; Main shadow color
:backdrop "rgba(0,0,0,0.4)" ; Backdrop for modals and bottom sheet
:border-01 "rgba(238,242,245,1)"
:border-02 "rgba(67, 96, 223, 0.1)"})
:border-02 "rgba(67, 96, 223, 0.1)"
:highlight "rgba(67,96,223,0.4)"})
(def dark-theme
{:positive-01 "rgba(68,208,88,1)"
@ -75,7 +76,8 @@
:shadow-01 "rgba(0,0,0,0.75)"
:backdrop "rgba(0,0,0,0.4)"
:border-01 "rgba(37,37,40,1)"
:border-02 "rgba(97,119,229,0.1)"})
:border-02 "rgba(97,119,229,0.1)"
:highlight "rgba(67,96,223,0.4)"})
(def theme (reagent/atom light-theme))

View File

@ -11,6 +11,7 @@
[status-im.utils.platform :as platform]))
(def maximum-image-size-px 2000)
(def max-images-batch 5)
(defn- resize-and-call [uri cb]
(react/image-get-size
@ -22,12 +23,17 @@
(if resize? maximum-image-size-px width)
(if resize? maximum-image-size-px height)
60
(fn [resized-image]
(let [path (aget resized-image "path")
(fn [^js resized-image]
(let [path (.-path resized-image)
path (if (string/starts-with? path "file") path (str "file://" path))]
(cb path)))
#(log/error "could not resize image" %))))))
(defn result->id [^js result]
(if platform/ios?
(.-localIdentifier result)
(.-path result)))
(re-frame/reg-fx
::save-image-to-gallery
(fn [base64-uri]
@ -41,8 +47,8 @@
width
height
100
(fn [resized-image]
(let [path (aget resized-image "path")
(fn [^js resized-image]
(let [path (.-path resized-image)
path (if (string/starts-with? path "file") path (str "file://" path))]
(.saveToCameraRoll CameraRoll path)))
#(log/error "could not resize image" %)))))))
@ -51,18 +57,27 @@
::chat-open-image-picker
(fn []
(react/show-image-picker
(fn [result]
(resize-and-call
(aget result "path")
#(re-frame/dispatch [:chat.ui/image-selected %])))
"photo")))
(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]))
(doseq [^js result (if platform/ios?
(take max-images-batch images)
[images])]
(resize-and-call (.-path result)
#(re-frame/dispatch [:chat.ui/image-selected (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 [uri]
(resize-and-call
uri
#(re-frame/dispatch [:chat.ui/image-selected %]))))
#(re-frame/dispatch [:chat.ui/image-selected uri %]))))
(re-frame/reg-fx
::camera-roll-get-photos
@ -72,7 +87,7 @@
:on-allowed (fn []
(-> (.getPhotos CameraRoll #js {:first num :assetType "Photos" :groupTypes "All"})
(.then #(re-frame/dispatch [:on-camera-roll-get-photos (:edges (types/js->clj %))]))
(.catch #(log/error "could not get cameraroll photos"))))})))
(.catch #(log/warn "could not get cameraroll photos"))))})))
(fx/defn image-captured
{:events [:chat.ui/image-captured]}
@ -89,27 +104,46 @@
[{db :db} photos]
{:db (assoc db :camera-roll-photos (mapv #(get-in % [:node :image :uri]) photos))})
(fx/defn cancel-sending-image
{:events [:chat.ui/cancel-sending-image]}
(fx/defn clear-sending-images
{:events [:chat.ui/clear-sending-images]}
[{:keys [db]}]
(let [current-chat-id (:current-chat-id db)]
{:db (update-in db [:chats current-chat-id :metadata] dissoc :sending-image)}))
{:db (update-in db [:chats current-chat-id :metadata] assoc :sending-image {})}))
(fx/defn cancel-sending-image
{:events [:chat.ui/cancel-sending-image]}
[cofx]
(clear-sending-images cofx))
(fx/defn image-selected
{:events [:chat.ui/image-selected]}
[{:keys [db]} uri]
[{:keys [db]} original uri]
(let [current-chat-id (:current-chat-id db)]
{:db (assoc-in db [:chats current-chat-id :metadata :sending-image :uri] uri)}))
{:db (update-in db [:chats current-chat-id :metadata :sending-image original] merge {:uri uri})}))
(fx/defn image-unselected
{:events [:chat.ui/image-unselected]}
[{:keys [db]} original]
(let [current-chat-id (:current-chat-id db)]
{:db (update-in db [:chats current-chat-id :metadata :sending-image] dissoc original)}))
(fx/defn chat-open-image-picker
{:events [:chat.ui/open-image-picker]}
[_]
{::chat-open-image-picker nil})
[{:keys [db]}]
(let [current-chat-id (:current-chat-id db)
images (get-in db [:chats current-chat-id :metadata :sending-image])]
(when (< (count images) max-images-batch)
{::chat-open-image-picker nil})))
(fx/defn camera-roll-pick
{:events [:chat.ui/camera-roll-pick]}
[_ uri]
{::image-selected uri})
[{:keys [db]} uri]
(let [current-chat-id (:current-chat-id db)
images (get-in db [:chats current-chat-id :metadata :sending-image])]
(when (and (< (count images) max-images-batch)
(not (get images uri)))
{:db (update-in db [:chats current-chat-id :metadata :sending-image] assoc uri {:uri uri})
::image-selected uri})))
(fx/defn save-image-to-gallery
{:events [:chat.ui/save-image-to-gallery]}

View File

@ -133,14 +133,17 @@
(fx/defn send-image
[{{:keys [current-chat-id] :as db} :db :as cofx}]
(let [image-path (get-in db [:chats current-chat-id :metadata :sending-image :uri])]
(let [images (get-in db [:chats current-chat-id :metadata :sending-image])]
(fx/merge cofx
;; NOTE(Ferossgp): Ideally here and for all other types of message we should dissoc on success only
{:db (update-in db [:chats current-chat-id :metadata] dissoc :sending-image)}
(when-not (string/blank? image-path)
(chat.message/send-message {:chat-id current-chat-id
:content-type constants/content-type-image
:image-path (utils/safe-replace image-path #"file://" "")
:text (i18n/label :t/update-to-see-image)})))))
(chat.message/send-messages
(map (fn [[_ {:keys [uri]}]]
{:chat-id current-chat-id
:content-type constants/content-type-image
:image-path (utils/safe-replace uri #"file://" "")
:text (i18n/label :t/update-to-see-image)})
images)))))
(fx/defn send-my-status-message
"when not empty, proceed by sending text message with public key topic"

View File

@ -233,8 +233,12 @@
(rebuild-message-list chat-id)))
(fx/defn send-message
[{:keys [db now] :as cofx} {:keys [chat-id] :as message}]
(protocol/send-chat-message cofx message))
[{:keys [db now] :as cofx} message]
(protocol/send-chat-messages cofx [message]))
(fx/defn send-messages
[{:keys [db now] :as cofx} messages]
(protocol/send-chat-messages cofx messages))
(fx/defn toggle-expand-message
[{:keys [db]} chat-id message-id]

View File

@ -5,31 +5,31 @@
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]))
(fx/defn send-chat-message [cofx {:keys [chat-id
text
response-to
ens-name
image-path
audio-path
audio-duration-ms
message-type
sticker
content-type]
:as message}]
{::json-rpc/call [{:method (json-rpc/call-ext-method
"sendChatMessage")
:params [{:chatId chat-id
:text text
:responseTo response-to
:ensName ens-name
:imagePath image-path
:audioPath audio-path
:audioDurationMs audio-duration-ms
:sticker sticker
:contentType content-type}]
:on-success
#(re-frame/dispatch [:transport/message-sent % 1])
:on-failure #(log/error "failed to send a message" %)}]})
(defn build-message [{:keys [chat-id
text
response-to
ens-name
image-path
audio-path
audio-duration-ms
sticker
content-type]}]
{:method (json-rpc/call-ext-method "sendChatMessage")
:params [{:chatId chat-id
:text text
:responseTo response-to
:ensName ens-name
:imagePath image-path
:audioPath audio-path
:audioDurationMs audio-duration-ms
:sticker sticker
:contentType content-type}]
:on-success
#(re-frame/dispatch [:transport/message-sent % 1])
:on-failure #(log/error "failed to send a message" %)})
(fx/defn send-chat-messages [cofx messages]
{::json-rpc/call (mapv build-message messages)})
(fx/defn send-reaction [cofx {:keys [message-id chat-id emoji-id]}]
{::json-rpc/call [{:method (json-rpc/call-ext-method

View File

@ -6,13 +6,14 @@
{:read-external-storage (cond
platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
:write-external-storage (cond
platform/android? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
platform/android? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS))
platform/ios? (.-PHOTO_LIBRARY (.-IOS PERMISSIONS)))
:camera (cond
platform/android? (.-CAMERA (.-ANDROID PERMISSIONS))
platform/ios? (.-CAMERA (.-IOS PERMISSIONS)))
platform/ios? (.-CAMERA (.-IOS PERMISSIONS)))
:record-audio (cond
platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS))
platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))})
platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))})
(defn all-granted? [permissions]
(let [permission-vals (distinct (vals permissions))]

View File

@ -192,9 +192,9 @@
(defn show-image-picker
([images-fn]
(show-image-picker images-fn nil))
([images-fn media-type]
([images-fn {:keys [multiple media-type]}]
(-> ^js image-picker
(.openPicker (clj->js {:multiple false :mediaType (or media-type "any")}))
(.openPicker (clj->js {:multiple multiple :mediaType (or media-type "any")}))
(.then images-fn)
(.catch show-access-error))))

View File

@ -287,7 +287,7 @@
{:style (styles/input-container)}
(when reply
[reply/reply-message reply])
(when sending-image
(when (seq sending-image)
[reply/send-image sending-image])
[rn/view {:style (styles/input-row)}
[text-input props]

View File

@ -71,13 +71,17 @@
[icons/icon :main-icons/close-circle {:container-style (styles/close-button)
:color (:icon-01 @colors/theme)}]]]]))
(defn send-image [{:keys [uri]}]
(defn send-image [images]
[rn/view {:style (styles/reply-container true)}
[rn/view {:style (styles/reply-content)}
[rn/image {:source {:uri uri}
:style {:width 56
:height 56
:border-radius 4}}]]
[rn/scroll-view {:horizontal true
:style (styles/reply-content)}
(for [{:keys [uri]} (vals images)]
^{:key uri}
[rn/image {:source {:uri uri}
:style {:width 56
:height 56
:border-radius 4
:margin-right 4}}])]
[rn/view
[pressable/pressable {:on-press #(re-frame/dispatch [:chat.ui/cancel-sending-image])
:accessibility-label :cancel-send-image}

View File

@ -2,13 +2,24 @@
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [status-im.ui.components.react :as react]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.ui.components.permissions :as permissions]
[reagent.core :as reagent]
[quo.components.animated.pressable :as pressable]
[re-frame.core :as re-frame]
[status-im.ui.components.colors :as colors]))
[quo.design-system.colors :as colors]
[status-im.chat.models.images :as images]
[quo.core :as quo]))
(defn take-picture []
(react/show-image-picker-camera #(re-frame/dispatch [:chat.ui/image-captured (.-path %)]) {}))
(permissions/request-permissions
{:permissions [:camera]
:on-allowed (fn []
(react/show-image-picker-camera #(re-frame/dispatch [:chat.ui/image-captured (.-path %)]) {}))}))
(defn show-image-picker []
(permissions/request-permissions
{:permissions [:read-external-storage :write-external-storage]
:on-allowed #(re-frame/dispatch [:chat.ui/open-image-picker])}))
(defn buttons []
[react/view
@ -18,26 +29,48 @@
[react/view {:style {:padding 10}}
[icons/icon :main-icons/camera]]]
[react/view {:style {:padding-top 8}}
[pressable/pressable {:on-press #(re-frame/dispatch [:chat.ui/open-image-picker])
[pressable/pressable {:on-press show-image-picker
:accessibility-label :open-gallery
:type :scale}
[react/view {:style {:padding 10}}
[icons/icon :main-icons/gallery]]]]])
(defn image-preview [uri first? panel-height]
(let [wh (/ (- panel-height 8) 2)]
[react/touchable-highlight {:on-press #(re-frame/dispatch [:chat.ui/camera-roll-pick uri])}
[react/image {:style (merge {:width wh
:height wh
:background-color :black
:resize-mode :cover
:border-radius 4}
(when first?
{:margin-bottom 8}))
:source {:uri uri}}]]))
(defn image-preview [uri all-selected first? panel-height]
(let [wh (/ (- panel-height 8) 2)
selected (get all-selected uri)
max-selected (>= (count all-selected) images/max-images-batch)]
[react/touchable-highlight {:on-press #(if selected
(re-frame/dispatch [:chat.ui/image-unselected uri])
(re-frame/dispatch [:chat.ui/camera-roll-pick uri]))
:disabled (and max-selected (not selected))}
[react/view {:style (merge {:width wh
:height wh
:border-radius 4
:overflow :hidden}
(when (and (not selected) max-selected)
{:opacity 0.5})
(when first?
{:margin-bottom 4}))}
[react/image {:style (merge {:width wh
:height wh
:background-color :black
:resize-mode :cover
:border-radius 4})
:source {:uri uri}}]
(when selected
[react/view {:style {:position :absolute
:top 0
:bottom 0
:left 0
:right 0
:padding 10
:background-color (:highlight @colors/theme)
:align-items :flex-end}}
[quo/radio {:value true}]])]]))
(defview photos []
(letsubs [camera-roll-photos [:camera-roll-photos]
selected [:chats/sending-image]
panel-height (reagent/atom nil)]
[react/view {:style {:flex 1
:flex-direction :row}
@ -45,16 +78,18 @@
(let [height @panel-height]
(for [[first-img second-img] (partition 2 camera-roll-photos)]
^{:key (str "image" first-img)}
[react/view {:margin-left 8}
[react/view {:margin-left 4}
(when first-img
[image-preview first-img true height])
[image-preview first-img selected true height])
(when second-img
[image-preview second-img false height])]))]))
[image-preview second-img selected false height])]))]))
(defview image-view []
{:component-did-mount (fn []
(re-frame/dispatch [:chat.ui/camera-roll-get-photos 20]))}
[react/animated-view {:style {:background-color colors/white
(permissions/request-permissions
{:permissions [:read-external-storage :write-external-storage]
:on-allowed #(re-frame/dispatch [:chat.ui/camera-roll-get-photos 20])}))}
[react/animated-view {:style {:background-color (:ui-background @colors/theme)
:flex 1}}
[react/scroll-view {:horizontal true :style {:flex 1}}
[react/view {:flex 1 :flex-direction :row :margin-horizontal 4}

View File

@ -218,19 +218,29 @@
[react/view (style/delivery-status outgoing)
[message-delivery-status message]]])
(defn message-content-image [{:keys [content outgoing]}]
(defn message-content-image [{:keys [content outgoing] :as message} {:keys [on-long-press]}]
(let [dimensions (reagent/atom [260 260])
uri (:image content)]
uri (:image content)]
(react/image-get-size
uri
(fn [width height]
(let [k (/ (max width height) 260)]
(reset! dimensions [(/ width k) (/ height k)]))))
(reset! dimensions [width height])))
(fn []
[react/view {:style (style/image-content outgoing)}
[react/image {:style {:width (first @dimensions) :height (last @dimensions)}
:resize-mode :contain
:source {:uri uri}}]])))
(let [k (/ (max (first @dimensions) (second @dimensions)) 260)
style-opts {:outgoing outgoing
:width (/ (first @dimensions) k)
:height (/ (second @dimensions) k)}]
[react/touchable-highlight {:on-press (fn []
(when (:image content)
(re-frame/dispatch [:navigate-to :image-preview message]))
(react/dismiss-keyboard!))
:on-long-press on-long-press}
[react/view {:style (style/image-message style-opts)}
[react/image {:style {:width (/ (first @dimensions) k)
:height (/ (second @dimensions) k)}
:resize-mode :contain
:source {:uri uri}}]
[react/view {:style (style/image-message-border style-opts)}]]]))))
(defmulti ->message :content-type)
@ -372,18 +382,13 @@
(defmethod ->message constants/content-type-image [{:keys [content] :as message} {:keys [on-long-press modal]
:as reaction-picker}]
[message-content-wrapper message
[react/touchable-highlight (when-not modal
{:on-press (fn [_]
(when (:image content)
(re-frame/dispatch [:navigate-to :image-preview message]))
(react/dismiss-keyboard!))
:on-long-press (fn []
(on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
:label (i18n/label :t/message-reply)}
{:on-press #(re-frame/dispatch [:chat.ui/save-image-to-gallery (:image content)])
:label (i18n/label :t/save)}]))})
[message-content-image message]]
[message-content-image message {:modal modal
:on-long-press (fn []
(on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
:label (i18n/label :t/message-reply)}
{:on-press #(re-frame/dispatch [:chat.ui/save-image-to-gallery (:image content)])
:label (i18n/label :t/save)}]))}]
reaction-picker])
(defmethod ->message constants/content-type-audio [message {:keys [on-long-press modal]

View File

@ -301,9 +301,21 @@
(outgoing-blockquote-text-style)
(default-blockquote-text-style)))
(defn image-content [outgoing]
{:overflow :hidden
(defn image-message
[{:keys [outgoing width height]}]
{:overflow "hidden"
:border-top-left-radius 16
:border-top-right-radius 16
:border-bottom-left-radius (if outgoing 16 4)
:border-bottom-right-radius (if outgoing 4 16)})
:border-bottom-right-radius (if outgoing 4 16)
:width width
:height height})
(defn image-message-border [opts]
(merge (image-message opts)
{:position :absolute
:top 0
:left 0
:background-color "transparent"
:border-width 1
:border-color colors/black-transparent}))