From cf4bef82682e55668bf21ded3c00b793b86c5dd3 Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 19 Oct 2020 16:02:19 +0300 Subject: [PATCH] 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 --- ios/Podfile.lock | 6 +- src/quo/design_system/colors.cljs | 6 +- src/status_im/chat/models/images.cljs | 74 ++++++++++++++----- src/status_im/chat/models/input.cljs | 15 ++-- src/status_im/chat/models/message.cljs | 8 +- src/status_im/transport/message/protocol.cljs | 50 ++++++------- src/status_im/ui/components/permissions.cljs | 7 +- src/status_im/ui/components/react.cljs | 4 +- .../ui/screens/chat/components/input.cljs | 2 +- .../ui/screens/chat/components/reply.cljs | 16 ++-- .../ui/screens/chat/image/views.cljs | 73 +++++++++++++----- .../ui/screens/chat/message/message.cljs | 45 ++++++----- .../screens/chat/styles/message/message.cljs | 18 ++++- 13 files changed, 212 insertions(+), 112 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8d5b9f10f1..583ee6a99a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/src/quo/design_system/colors.cljs b/src/quo/design_system/colors.cljs index da38745b66..c7348319f0 100644 --- a/src/quo/design_system/colors.cljs +++ b/src/quo/design_system/colors.cljs @@ -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)) diff --git a/src/status_im/chat/models/images.cljs b/src/status_im/chat/models/images.cljs index fa018edd7e..bb9402b741 100644 --- a/src/status_im/chat/models/images.cljs +++ b/src/status_im/chat/models/images.cljs @@ -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]} diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index abbe833c8b..9a93c07aa1 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -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" diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index a2cb5795d0..1c21d0c354 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -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] diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 3501c147c4..6f66a26269 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -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 diff --git a/src/status_im/ui/components/permissions.cljs b/src/status_im/ui/components/permissions.cljs index 26132ea364..61fdd07c99 100644 --- a/src/status_im/ui/components/permissions.cljs +++ b/src/status_im/ui/components/permissions.cljs @@ -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))] diff --git a/src/status_im/ui/components/react.cljs b/src/status_im/ui/components/react.cljs index b2ff393854..58ce370ab1 100644 --- a/src/status_im/ui/components/react.cljs +++ b/src/status_im/ui/components/react.cljs @@ -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)))) diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs index 3fc18d2ca7..1382034a64 100644 --- a/src/status_im/ui/screens/chat/components/input.cljs +++ b/src/status_im/ui/screens/chat/components/input.cljs @@ -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] diff --git a/src/status_im/ui/screens/chat/components/reply.cljs b/src/status_im/ui/screens/chat/components/reply.cljs index 23100dd891..d1f0fe0ddf 100644 --- a/src/status_im/ui/screens/chat/components/reply.cljs +++ b/src/status_im/ui/screens/chat/components/reply.cljs @@ -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} diff --git a/src/status_im/ui/screens/chat/image/views.cljs b/src/status_im/ui/screens/chat/image/views.cljs index b2138dd7b7..57e710b85f 100644 --- a/src/status_im/ui/screens/chat/image/views.cljs +++ b/src/status_im/ui/screens/chat/image/views.cljs @@ -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} diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index 834a2374eb..913834b2eb 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -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] diff --git a/src/status_im/ui/screens/chat/styles/message/message.cljs b/src/status_im/ui/screens/chat/styles/message/message.cljs index 7b07bee374..812355e5bb 100644 --- a/src/status_im/ui/screens/chat/styles/message/message.cljs +++ b/src/status_im/ui/screens/chat/styles/message/message.cljs @@ -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}))