Photo selector and album selector screens (#14867)

* feat: photo & album selector screens
This commit is contained in:
Omar Basem 2023-02-01 17:01:03 +04:00 committed by GitHub
parent fb9309f700
commit b8eab0c328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 458 additions and 249 deletions

View File

@ -14,7 +14,7 @@
counter-value -> number counter-value -> number
increase-padding-top? -> boolean increase-padding-top? -> boolean
blur? -> boolean" blur? -> boolean"
[{:keys [label chevron-position counter-value increase-padding-top? blur?]}] [{:keys [label chevron-position counter-value increase-padding-top? blur? container-style]}]
(let [dark? (colors/dark?) (let [dark? (colors/dark?)
border-and-counter-bg-color (if dark? border-and-counter-bg-color (if dark?
(if blur? colors/white-opa-5 colors/neutral-70) (if blur? colors/white-opa-5 colors/neutral-70)
@ -25,12 +25,13 @@
[rn/view [rn/view
{:accessible true {:accessible true
:accessibility-label :divider-label :accessibility-label :divider-label
:style {:border-top-width 1 :style (merge {:border-top-width 1
:border-top-color border-and-counter-bg-color :border-top-color border-and-counter-bg-color
:padding-top padding-top :padding-top padding-top
:padding-horizontal 16 :padding-horizontal 16
:align-items :center :align-items :center
:flex-direction :row}} :flex-direction :row}
container-style)}
(when (= chevron-position :left) (when (= chevron-position :left)
[rn/view [rn/view
{:test-ID :divider-label-icon-left {:test-ID :divider-label-icon-left

View File

@ -9,7 +9,7 @@
(def ^:private themes (def ^:private themes
{:container {:dark {:background-color colors/white-opa-70} {:container {:dark {:background-color colors/white-opa-70}
:light {:background-color colors/neutral-80-opa-70}} :light {:background-color colors/neutral-80-opa-90}}
:text {:dark {:color colors/neutral-100} :text {:dark {:color colors/neutral-100}
:light {:color colors/white}} :light {:color colors/white}}
:icon {:dark {:color colors/neutral-100} :icon {:dark {:color colors/neutral-100}
@ -51,8 +51,8 @@
[i18n/label :t/undo]]]) [i18n/label :t/undo]]])
(defn- toast-container (defn- toast-container
[{:keys [left middle right]}] [{:keys [left middle right container-style]}]
[rn/view {:style {:padding-left 12 :padding-right 12}} [rn/view {:style (merge {:padding-left 12 :padding-right 12} container-style)}
[rn/view [rn/view
{:style (merge-theme-style :container {:style (merge-theme-style :container
{:flex-direction :row {:flex-direction :row
@ -74,14 +74,15 @@
(when right right)]]) (when right right)]])
(defn toast (defn toast
[{:keys [icon icon-color text action undo-duration undo-on-press]}] [{:keys [icon icon-color text action undo-duration undo-on-press container-style]}]
[toast-container [toast-container
{:left (when icon {:left (when icon
[icon/icon icon [icon/icon icon
{:container-style {:width 20 :height 20} {:container-style {:width 20 :height 20}
:color (or icon-color :color (or icon-color
(get-in themes [:icon (theme/get-theme) :color]))}]) (get-in themes [:icon (theme/get-theme) :color]))}])
:middle text :middle text
:right (if undo-duration :right (if undo-duration
[toast-undo-action undo-duration undo-on-press] [toast-undo-action undo-duration undo-on-press]
action)}]) action)
:container-style container-style}])

View File

@ -104,7 +104,7 @@
(re-frame/reg-fx (re-frame/reg-fx
::camera-roll-get-photos ::camera-roll-get-photos
(fn [[num end-cursor]] (fn [[num end-cursor album]]
(permissions/request-permissions (permissions/request-permissions
{:permissions [:read-external-storage] {:permissions [:read-external-storage]
:on-allowed (fn [] :on-allowed (fn []
@ -115,19 +115,69 @@
{:first num {:first num
:after end-cursor :after end-cursor
:assetType "Photos" :assetType "Photos"
:groupTypes "All" :groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums")
:groupName (when (not= album (i18n/label :t/recent)) album)
:include (clj->js ["imageSize"])}) :include (clj->js ["imageSize"])})
(.getPhotos CameraRoll (.getPhotos
#js CameraRoll
{:first num #js
:assetType "Photos" {:first num
:groupTypes "All" :assetType "Photos"
:include (clj->js ["imageSize"])})) :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 %)] (.then #(let [response (types/js->clj %)]
(re-frame/dispatch [:on-camera-roll-get-photos (:edges response) (re-frame/dispatch [:on-camera-roll-get-photos (:edges response)
(:page_info response) end-cursor]))) (:page_info response) end-cursor])))
(.catch #(log/warn "could not get camera roll photos"))))}))) (.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]
@ -139,15 +189,20 @@
(rf/defn on-end-reached (rf/defn on-end-reached
{:events [:camera-roll/on-end-reached]} {:events [:camera-roll/on-end-reached]}
[_ end-cursor loading? has-next-page?] [_ end-cursor selected-album loading? has-next-page?]
(when (and (not 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-loading-more true])
(re-frame/dispatch [:chat.ui/camera-roll-get-photos 20 end-cursor]))) (re-frame/dispatch [:chat.ui/camera-roll-get-photos 20 end-cursor selected-album])))
(rf/defn camera-roll-get-photos (rf/defn camera-roll-get-photos
{:events [:chat.ui/camera-roll-get-photos]} {:events [:chat.ui/camera-roll-get-photos]}
[_ num end-cursor] [_ num end-cursor selected-album]
{::camera-roll-get-photos [num end-cursor]}) {::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 (rf/defn camera-roll-loading-more
{:events [:chat.ui/camera-roll-loading-more]} {:events [:chat.ui/camera-roll-loading-more]}
@ -164,22 +219,32 @@
(assoc :camera-roll/has-next-page (:has_next_page page-info)) (assoc :camera-roll/has-next-page (:has_next_page page-info))
(assoc :camera-roll/loading-more false))})) (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]} current-chat-id] [{:keys [db]}]
{:db (update-in db [:chat/inputs current-chat-id :metadata] assoc :sending-image {})}) {:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})})
(rf/defn cancel-sending-image (rf/defn cancel-sending-image
{:events [:chat.ui/cancel-sending-image]} {:events [:chat.ui/cancel-sending-image]}
[{:keys [db] :as cofx} chat-id] [{:keys [db] :as cofx} chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))] (clear-sending-images cofx))
(clear-sending-images cofx current-chat-id)))
(rf/defn image-selected (rf/defn image-selected
{:events [:chat.ui/image-selected]} {:events [:chat.ui/image-selected]}
[{:keys [db]} current-chat-id original uri] [{:keys [db]} current-chat-id original uri]
{:db {:db
(update-in db [:chat/inputs current-chat-id :metadata :sending-image uri] merge original {:uri uri})}) (update-in db
[:chat/inputs current-chat-id :metadata :sending-image (:uri original)]
merge
original
{:resized-uri uri})})
(rf/defn image-unselected (rf/defn image-unselected
{:events [:chat.ui/image-unselected]} {:events [:chat.ui/image-unselected]}
@ -215,6 +280,11 @@
(not (some #(= (:uri image) (:uri %)) images))) (not (some #(= (:uri image) (:uri %)) images)))
{::image-selected [image current-chat-id]})))) {::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 (rf/defn save-image-to-gallery
{:events [:chat.ui/save-image-to-gallery]} {:events [:chat.ui/save-image-to-gallery]}
[_ base64-uri] [_ base64-uri]

View File

@ -33,7 +33,7 @@
(rf/defn set-chat-input-text (rf/defn set-chat-input-text
"Set input text for current-chat. Takes db and input text and cofx "Set input text for current-chat. Takes db and input text and cofx
as arguments and returns new fx. Always clear all validation messages." as arguments and returns new fx. Always clear all validation messages."
{:events [:chat.ui/set-chat-input-text]} {:events [:chat.ui/set-chat-input-text]}
[{db :db} new-input chat-id] [{db :db} new-input chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))] (let [current-chat-id (or chat-id (:current-chat-id db))]
@ -150,11 +150,11 @@
[{db :db} chat-id input-text] [{db :db} chat-id input-text]
(let [images (get-in db [:chat/inputs chat-id :metadata :sending-image]) (let [images (get-in db [:chat/inputs chat-id :metadata :sending-image])
album-id (str (random-uuid))] album-id (str (random-uuid))]
(mapv (fn [[_ {:keys [uri width height]}]] (mapv (fn [[_ {:keys [resized-uri width height]}]]
{:chat-id chat-id {:chat-id chat-id
:album-id album-id :album-id album-id
:content-type constants/content-type-image :content-type constants/content-type-image
:image-path (utils/safe-replace uri #"file://" "") :image-path (utils/safe-replace resized-uri #"file://" "")
:image-width width :image-width width
:image-height height :image-height height
;; TODO: message not received if text field is ;; TODO: message not received if text field is

View File

@ -161,8 +161,10 @@
(rf/defn on-going-in-background (rf/defn on-going-in-background
[{:keys [db now]}] [{:keys [db now]}]
{:db (assoc db :app-in-background-since now) {:db (assoc db :app-in-background-since now)
:dispatch-n [[:audio-recorder/on-background] [:audio-message/on-background]]}) ;; event not implemented
;; :dispatch-n [[:audio-recorder/on-background] [:audio-message/on-background]]
})
(rf/defn app-state-change (rf/defn app-state-change
{:events [:app-state-change]} {:events [:app-state-change]}

View File

@ -90,8 +90,10 @@
{:style {:padding-top 16 {:style {:padding-top 16
:align-items :center}} :align-items :center}}
[rn/touchable-opacity [rn/touchable-opacity
{:on-press #(rf/dispatch [:bottom-sheet/show-sheet {:on-press (fn []
{:content (bottom-sheet (boolean image) editing?)}])} (rn/dismiss-keyboard!)
(rf/dispatch [:bottom-sheet/show-sheet
{:content (bottom-sheet (boolean image) editing?)}]))}
[rn/view [rn/view
{:style {:width 128 {:style {:width 128
:height 128}} :height 128}}

View File

@ -11,7 +11,7 @@
;; so we need some magic here with paddings so close button isn't cut ;; so we need some magic here with paddings so close button isn't cut
[rn/view {:padding-top 12 :padding-bottom 8 :padding-right 12} [rn/view {:padding-top 12 :padding-bottom 8 :padding-right 12}
[rn/image [rn/image
{:source {:uri (:uri (val item))} {:source {:uri (:resized-uri (val item))}
:style style/small-image}] :style style/small-image}]
[rn/touchable-opacity [rn/touchable-opacity
{:on-press (fn [] (rf/dispatch [:chat.ui/image-unselected (val item)])) {:on-press (fn [] (rf/dispatch [:chat.ui/image-unselected (val item)]))

View File

@ -207,27 +207,30 @@
(def ^:const delete-message-undo-time-limit-ms 4000) (def ^:const delete-message-undo-time-limit-ms 4000)
(def ^:const delete-message-for-me-undo-time-limit-ms 4000) (def ^:const delete-message-for-me-undo-time-limit-ms 4000)
(def ^:const album-image-sizes (def ^:const max-album-photos 6)
{2 {0 146 (def ^:const image-size 146)
1 146}
3 {0 [292 194] (def album-image-sizes ; sometimes we subtract 1 or 0.5 for padding
1 [145 97] {2 {0 image-size
2 [145 97]} 1 image-size}
4 {0 146 3 {0 [(* image-size 2) (* image-size 1.5)]
1 146 1 [(- image-size 1) (- (* image-size 0.67) 1)]
2 146 2 [(- image-size 1) (- (* image-size 0.67) 1)]}
3 146} 4 {0 image-size
5 {0 146 1 image-size
1 146 2 image-size
2 97 3 image-size}
3 97 5 {0 image-size
4 97} 1 image-size
:default {0 146 2 (- (* image-size 0.67) 1)
1 146 3 (- (* image-size 0.67) 1)
2 72.5 4 (- (* image-size 0.67) 1)}
3 72.5 :default {0 image-size
4 72.5 1 image-size
5 72.5}}) 2 (- (* image-size 0.5) 0.5)
3 (- (* image-size 0.5) 0.5)
4 (- (* image-size 0.5) 0.5)
5 (- (* image-size 0.5) 0.5)}})
(def ^:const spam-message-frequency-threshold 4) (def ^:const spam-message-frequency-threshold 4)
(def ^:const spam-interval-ms 1000) (def ^:const spam-interval-ms 1000)

View File

@ -324,10 +324,12 @@
(rf/dispatch [:bottom-sheet/hide]) (rf/dispatch [:bottom-sheet/hide])
(rf/dispatch [:chat.ui/remove-chat chat-id]))}}) (rf/dispatch [:chat.ui/remove-chat chat-id]))}})
(rf/defn navigate-to-horizontal-images (rf/defn navigate-to-user-pinned-messages
{:events [:chat.ui/navigate-to-horizontal-images]} "Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
[cofx messages index] {:events [:chat.ui/navigate-to-pinned-messages]}
(navigation/navigate-to cofx :images-horizontal {:messages messages :index index})) [cofx chat-id]
(navigation/navigate-to cofx :chat-pinned-messages {:chat-id chat-id}))
(rf/defn update-shared-element-id (rf/defn update-shared-element-id
{:events [:chat.ui/update-shared-element-id]} {:events [:chat.ui/update-shared-element-id]}

View File

@ -1,4 +1,4 @@
(ns status-im2.contexts.chat.images-horizontal.style (ns status-im2.contexts.chat.lightbox.style
(:require [quo2.foundations.colors :as colors])) (:require [quo2.foundations.colors :as colors]))
(defn container-view (defn container-view

View File

@ -1,12 +1,13 @@
(ns status-im2.contexts.chat.images-horizontal.view (ns status-im2.contexts.chat.lightbox.view
(:require [quo2.core :as quo] (:require [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.fast-image :as fast-image]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[react-native.safe-area :as safe-area] [react-native.safe-area :as safe-area]
[reagent.core :as reagent] [reagent.core :as reagent]
[oops.core :as oops] [oops.core :as oops]
[status-im2.contexts.chat.images-horizontal.style :as style] [status-im2.contexts.chat.lightbox.style :as style]
[utils.datetime :as datetime] [utils.datetime :as datetime]
[react-native.linear-gradient :as linear-gradient])) [react-native.linear-gradient :as linear-gradient]))
@ -20,7 +21,7 @@
window-width (:width (rn/get-window)) window-width (:width (rn/get-window))
height (* (or (:image-height message) 1000) height (* (or (:image-height message) 1000)
(/ window-width (or (:image-width message) 1000)))] (/ window-width (or (:image-width message) 1000)))]
[rn/image [fast-image/fast-image
{:source {:uri (:image (:content message))} {:source {:uri (:image (:content message))}
:style {:width window-width :style {:width window-width
:height height :height height
@ -77,7 +78,7 @@
(defn small-image (defn small-image
[item] [item]
[rn/image [fast-image/fast-image
{:source {:uri (:image (:content item))} {:source {:uri (:image (:content item))}
:style {:width small-image-size :style {:width small-image-size
:height small-image-size :height small-image-size
@ -105,7 +106,7 @@
:content-container-style {:padding-vertical 12 :content-container-style {:padding-vertical 12
:padding-horizontal padding-horizontal}}]])) :padding-horizontal padding-horizontal}}]]))
(defn images-horizontal (defn lightbox
[] []
(let [{:keys [messages index]} (rf/sub [:get-screen-params]) (let [{:keys [messages index]} (rf/sub [:get-screen-params])
;; The initial value of data is the image that was pressed (and not the whole album) in order for ;; The initial value of data is the image that was pressed (and not the whole album) in order for

View File

@ -5,7 +5,6 @@
[utils.i18n :as i18n] [utils.i18n :as i18n]
[quo2.core :as quo] [quo2.core :as quo]
[status-im2.common.not-implemented :as not-implemented] [status-im2.common.not-implemented :as not-implemented]
[status-im2.contexts.chat.photo-selector.view :as photo-selector]
[status-im2.contexts.chat.messages.composer.controls.style :as style] [status-im2.contexts.chat.messages.composer.controls.style :as style]
[status-im2.contexts.chat.messages.list.view :as messages.list] [status-im2.contexts.chat.messages.list.view :as messages.list]
[status-im.ui.components.permissions :as permissions] [status-im.ui.components.permissions :as permissions]
@ -45,9 +44,7 @@
(permissions/request-permissions (permissions/request-permissions
{:permissions [:read-external-storage :write-external-storage] {:permissions [:read-external-storage :write-external-storage]
:on-allowed #(rf/dispatch :on-allowed #(rf/dispatch
[:bottom-sheet/show-sheet [:open-modal :photo-selector {:chat-id chat-id}])
{:content (fn []
(photo-selector/photo-selector chat-id))}])
:on-denied (fn [] :on-denied (fn []
(background-timer/set-timeout (background-timer/set-timeout
#(utils-old/show-popup (i18n/label :t/error) #(utils-old/show-popup (i18n/label :t/error)

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.chat.messages.content.album.style (ns status-im2.contexts.chat.messages.content.album.style
(:require [quo2.foundations.colors :as colors])) (:require [quo2.foundations.colors :as colors]
[status-im2.constants :as constants]))
(def max-album-height 292) (def max-album-height 292)
@ -7,15 +8,18 @@
[portrait?] [portrait?]
{:flex-direction (if portrait? :column :row) {:flex-direction (if portrait? :column :row)
:flex-wrap :wrap :flex-wrap :wrap
:max-height max-album-height}) :max-height max-album-height
:border-radius 12
:width (inc (* constants/image-size 2))
:overflow :hidden})
(defn image (defn image
[dimensions index] [dimensions index portrait?]
{:width (:width dimensions) {:width (:width dimensions)
:height (:height dimensions) :height (:height dimensions)
:margin-left (when (or (and (not= index 0) (not= index 2) (not= count 3)) :margin-left (when (or (and (not= index 0) (not= index 2) (not= count 3))
(= count 3) (= count 3)
(= index 2)) (and portrait? (= index 2)))
1) 1)
:margin-bottom (when (< index 2) 1) :margin-bottom (when (< index 2) 1)
:align-self :flex-start}) :align-self :flex-start})

View File

@ -2,11 +2,11 @@
(:require [quo2.core :as quo] (:require [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.fast-image :as fast-image]
[status-im2.contexts.chat.messages.content.album.style :as style] [status-im2.contexts.chat.messages.content.album.style :as style]
[status-im2.constants :as constants] [status-im2.constants :as constants]
[utils.re-frame :as rf] [utils.re-frame :as rf]
[status-im2.contexts.chat.messages.content.image.view :as image])) [status-im2.contexts.chat.messages.content.image.view :as image]))
(def max-display-count 6)
(def rectangular-style-count 3) (def rectangular-style-count 3)
@ -32,7 +32,7 @@
(defn border-brr (defn border-brr
[index count] [index count]
(when (or (and (= index 1) (< count rectangular-style-count)) (when (or (and (= index 1) (< count rectangular-style-count))
(and (= index (- (min count max-display-count) 1)) (> count 2))) (and (= index (- (min count constants/max-album-photos) 1)) (> count 2)))
12)) 12))
(defn find-size (defn find-size
@ -42,7 +42,7 @@
{:width (second size-arr) :height (first size-arr) :album-style album-style})) {:width (second size-arr) :height (first size-arr) :album-style album-style}))
(defn album-message (defn album-message
[{:keys [albumize?] :as message} {:keys [on-long-press] :as context}] [{:keys [albumize?] :as message}]
(let [shared-element-id (rf/sub [:shared-element-id]) (let [shared-element-id (rf/sub [:shared-element-id])
first-image (first (:album message)) first-image (first (:album message))
album-style (if (> (:image-width first-image) (:image-height first-image)) album-style (if (> (:image-width first-image) (:image-height first-image))
@ -57,7 +57,7 @@
{:style (style/album-container portrait?)} {:style (style/album-container portrait?)}
(map-indexed (map-indexed
(fn [index item] (fn [index item]
(let [images-size-key (if (< images-count max-display-count) images-count :default) (let [images-size-key (if (< images-count constants/max-album-photos) images-count :default)
size (get-in constants/album-image-sizes [images-size-key index]) size (get-in constants/album-image-sizes [images-size-key index])
dimensions (if (not= images-count rectangular-style-count) dimensions (if (not= images-count rectangular-style-count)
{:width size :height size} {:width size :height size}
@ -67,22 +67,17 @@
:active-opacity 1 :active-opacity 1
:on-press (fn [] :on-press (fn []
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)]) (rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)])
(js/setTimeout #(rf/dispatch [:chat.ui/navigate-to-horizontal-images (js/setTimeout #(rf/dispatch [:navigate-to :lightbox
(:album message) index]) {:messages (:album message) :index index}])
100)) 100))}
:on-long-press on-long-press} [fast-image/fast-image
[rn/image {:style (style/image dimensions index portrait?)
{:style (merge
(style/image dimensions index)
{:border-top-left-radius (border-tlr index)
:border-top-right-radius (border-trr index images-count album-style)
:border-bottom-left-radius (border-blr index images-count album-style)
:border-bottom-right-radius (border-brr index images-count)})
:source {:uri (:image (:content item))} :source {:uri (:image (:content item))}
:native-ID (when (and (= shared-element-id (:message-id item)) :native-ID (when (and (= shared-element-id (:message-id item))
(< index max-display-count)) (< index constants/max-album-photos))
:shared-element)}] :shared-element)}]
(when (and (> images-count max-display-count) (= index (- max-display-count 1))) (when (and (> images-count constants/max-album-photos)
(= index (- constants/max-album-photos 1)))
[rn/view [rn/view
{:style (merge style/overlay {:style (merge style/overlay
{:border-bottom-right-radius (border-brr index images-count)})} {:border-bottom-right-radius (border-brr index images-count)})}
@ -90,11 +85,10 @@
{:weight :bold {:weight :bold
:size :heading-2 :size :heading-2
:style {:color colors/white}} :style {:color colors/white}}
(str "+" (- images-count (dec max-display-count)))]])])) (str "+" (- images-count (dec constants/max-album-photos)))]])]))
(:album message))] (:album message))]
[:<> [:<>
(map-indexed (map-indexed
(fn [index item] (fn [index item]
[image/image-message index item context]) [image/image-message index item])
(:album message))]))) (:album message))])))

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.chat.messages.content.image.view (ns status-im2.contexts.chat.messages.content.image.view
(:require [react-native.core :as rn] (:require [react-native.core :as rn]
[react-native.fast-image :as fast-image]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn calculate-dimensions (defn calculate-dimensions
@ -15,24 +16,24 @@
{:width calculated-width :height calculated-height})))) {:width calculated-width :height calculated-height}))))
(defn image-message (defn image-message
[index {:keys [content image-width image-height message-id] :as message} {:keys [on-long-press]}] [index {:keys [content image-width image-height message-id] :as message}]
(let [dimensions (calculate-dimensions (or image-width 1000) (or image-height 1000)) (let [dimensions (calculate-dimensions (or image-width 1000) (or image-height 1000))
text (:text content)] text (:text content)]
(fn [] (fn []
(let [shared-element-id (rf/sub [:shared-element-id])] (let [shared-element-id (rf/sub [:shared-element-id])]
[rn/touchable-opacity [rn/touchable-opacity
{:style {:margin-top (when (> index 0) 20) :width (:width dimensions)} {:active-opacity 1
:on-press (fn [] :key message-id
(rf/dispatch [:chat.ui/update-shared-element-id message-id]) :style {:margin-top (when (> index 0) 20)}
(js/setTimeout #(rf/dispatch [:chat.ui/navigate-to-horizontal-images :on-press (fn []
[message] 0]) (rf/dispatch [:chat.ui/update-shared-element-id message-id])
100)) (js/setTimeout #(rf/dispatch [:navigate-to :lightbox
:on-long-press on-long-press} {:messages [message] :index 0}])
[rn/view 100))}
;; This text comp is temporary. Should later use ;; This text comp is temporary. Should later use
;; `status-im2.contexts.chat.messages.content.text.view` ;; `status-im2.contexts.chat.messages.content.text.view`
(when (and (not= text "placeholder") (= index 0)) [rn/text text]) (when (and (not= text "placeholder") (= index 0)) [rn/text text])
[rn/image [fast-image/fast-image
{:source {:uri (:image content)} {:source {:uri (:image content)}
:style (merge dimensions {:border-radius 12}) :style (merge dimensions {:border-radius 12})
:native-ID (when (= shared-element-id message-id) :shared-element)}]]])))) :native-ID (when (= shared-element-id message-id) :shared-element)}]]))))

View File

@ -1,6 +1,7 @@
(ns status-im2.contexts.chat.messages.content.view (ns status-im2.contexts.chat.messages.content.view
(:require [react-native.core :as rn] (:require [react-native.core :as rn]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[status-im.utils.utils :as utils]
[status-im2.contexts.chat.messages.content.style :as style] [status-im2.contexts.chat.messages.content.style :as style]
[status-im2.contexts.chat.messages.content.pin.view :as pin] [status-im2.contexts.chat.messages.content.pin.view :as pin]
[status-im2.constants :as constants] [status-im2.constants :as constants]
@ -16,8 +17,7 @@
[utils.re-frame :as rf] [utils.re-frame :as rf]
[status-im.ui2.screens.chat.messages.message :as old-message] [status-im.ui2.screens.chat.messages.message :as old-message]
[status-im2.common.not-implemented :as not-implemented] [status-im2.common.not-implemented :as not-implemented]
[utils.datetime :as datetime] [utils.datetime :as datetime]))
[status-im.utils.utils :as utils]))
(defn avatar (defn avatar
[{:keys [content last-in-group? pinned quoted-message from]}] [{:keys [content last-in-group? pinned quoted-message from]}]

View File

@ -0,0 +1,22 @@
(ns status-im2.contexts.chat.photo-selector.album-selector.style
(:require [quo2.foundations.colors :as colors]))
(defn album-container
[selected?]
{:flex-direction :row
:padding-horizontal 12
:padding-vertical 8
:margin-horizontal 8
:border-radius 12
:align-items :center
:background-color (when selected? colors/primary-50-opa-5)})
(def cover
{:width 40
:height 40
:border-radius 10})
(def divider
{:padding-horizontal 20
:margin-top 16
:margin-bottom 8})

View File

@ -0,0 +1,64 @@
(ns status-im2.contexts.chat.photo-selector.album-selector.view
(:require
[quo2.core :as quo]
[react-native.core :as rn]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[quo2.foundations.colors :as colors]
[status-im2.contexts.chat.photo-selector.view :refer [album-title]]
[status-im2.contexts.chat.photo-selector.album-selector.style :as style]))
(defn album
[{:keys [title count uri]} index _ selected-album]
(let [selected? (= selected-album title)]
[rn/touchable-opacity
{:on-press (fn []
(rf/dispatch [:chat.ui/camera-roll-select-album title])
(rf/dispatch [:navigate-back]))
:style (style/album-container selected?)
:accessibility-label (str "album-" index)}
[rn/image
{:source {:uri uri}
:style style/cover}]
[rn/view {:style {:margin-left 12}}
[quo/text {:weight :medium} title]
[quo/text
{:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}}
(str count " " (i18n/label :t/images))]]
(when selected?
[rn/view
{:style {:position :absolute
:right 16}}
[quo/icon :i/check
{:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60)}]])]))
(defn section-header
[{:keys [title]}]
(when (not= title "smart-albums")
[quo/divider-label
{:label title
:container-style style/divider}]))
(defn album-selector
[]
[:f>
(fn []
(let [albums (rf/sub [:camera-roll/albums])
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
(rn/use-effect-once
(fn []
(rf/dispatch [:chat.ui/camera-roll-get-albums])
js/undefined))
[rn/view {:style {:padding-top 20}}
[album-title false selected-album]
[rn/section-list
{:data albums
:render-fn album
:render-data selected-album
:sections albums
:sticky-section-headers-enabled false
:render-section-header-fn section-header
:style {:margin-top 12}
:content-container-style {:padding-bottom 40}
:key-fn :title}]]))])

View File

@ -3,28 +3,31 @@
[react-native.platform :as platform])) [react-native.platform :as platform]))
(defn gradient-container (defn gradient-container
[safe-area] [insets]
{:width "100%" {:left 0
:height (+ (:bottom safe-area) 65) :right 0
:height (+ (:bottom insets) (if platform/ios? 65 85))
:position :absolute :position :absolute
:bottom (if platform/ios? 0 (:bottom safe-area))}) :bottom 0})
(defn buttons-container (def buttons-container
[safe-area] {:position :absolute
{:flex-direction :row :flex-direction :row
:justify-content :space-between :left 0
:padding-horizontal 20 :right 0
:bottom (+ (:bottom safe-area) 33)}) :margin-top 20
:margin-bottom 12
:justify-content :center
:z-index 1})
(defn clear-container (defn clear-container
[] []
{:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80) {:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80)
:position :absolute
:align-self :flex-end
:padding-horizontal 12 :padding-horizontal 12
:padding-vertical 5 :padding-vertical 5
:right 20 :border-radius 10
:border-radius 10}) :position :absolute
:right 20})
(defn camera-button-container (defn camera-button-container
[] []
@ -34,17 +37,21 @@
:border-radius 10 :border-radius 10
:justify-content :center :justify-content :center
:align-items :center :align-items :center
:margin-left 20 :position :absolute
:margin-bottom 24}) :left 20})
(def title-container (defn title-container
{:flex-direction :row []
:position :absolute {:flex-direction :row
:align-self :center}) :background-color (colors/theme-colors colors/neutral-10 colors/neutral-80)
:border-radius 10
:padding-horizontal 12
:padding-vertical 5
:align-self :center})
(defn chevron-container (defn chevron-container
[] []
{:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80) {:background-color (colors/theme-colors colors/neutral-30 colors/neutral-100)
:width 14 :width 14
:height 14 :height 14
:border-radius 7 :border-radius 7
@ -55,10 +62,12 @@
(defn image (defn image
[window-width index] [window-width index]
{:width (- (/ window-width 3) 0.67) {:width (- (/ window-width 3) 0.67)
:height (/ window-width 3) :height (/ window-width 3)
:margin-left (when (not= (mod index 3) 0) 1) :margin-left (when (not= (mod index 3) 0) 1)
:margin-bottom 1}) :margin-bottom 1
:border-top-left-radius (when (= index 0) 10)
:border-top-right-radius (when (= index 2) 10)})
(defn overlay (defn overlay
[window-width] [window-width]

View File

@ -1,48 +1,46 @@
(ns status-im2.contexts.chat.photo-selector.view (ns status-im2.contexts.chat.photo-selector.view
(:require [utils.i18n :as i18n] (:require
[quo.components.safe-area :as safe-area] [react-native.platform :as platform]
[quo2.components.notifications.info-count :as info-count] [status-im2.constants :as constants]
[quo2.core :as quo] [utils.i18n :as i18n]
[quo2.foundations.colors :as colors] [react-native.safe-area :as safe-area]
[react-native.core :as rn] [quo2.components.notifications.info-count :as info-count]
[react-native.linear-gradient :as linear-gradient] [quo2.core :as quo]
[reagent.core :as reagent] [quo2.foundations.colors :as colors]
[status-im2.contexts.chat.photo-selector.style :as style] [react-native.core :as rn]
[status-im.utils.core :as utils] [react-native.linear-gradient :as linear-gradient]
[utils.re-frame :as rf])) [reagent.core :as reagent]
[status-im2.contexts.chat.photo-selector.style :as style]
(def selected (reagent/atom [])) [status-im.utils.core :as utils]
[quo.react]
[utils.re-frame :as rf]))
(defn on-press-confirm-selection (defn on-press-confirm-selection
[chat-id] [selected]
(rf/dispatch [:chat.ui/clear-sending-images chat-id]) (rf/dispatch [:chat.ui/clear-sending-images])
(doseq [item @selected] (doseq [item @selected]
(rf/dispatch [:chat.ui/camera-roll-pick item])) (rf/dispatch [:chat.ui/camera-roll-pick item]))
(reset! selected []) (reset! selected [])
(rf/dispatch [:bottom-sheet/hide])) (rf/dispatch [:navigate-back]))
(defn bottom-gradient (defn bottom-gradient
[chat-id selected-images] [selected-images insets selected]
[:f> (when (or (seq @selected) (seq selected-images))
(fn [] [linear-gradient/linear-gradient
(let [safe-area (safe-area/use-safe-area)] {:colors [:black :transparent]
(when (or (seq @selected) selected-images) :start {:x 0 :y 1}
[linear-gradient/linear-gradient :end {:x 0 :y 0}
{:colors [:black :transparent] :style (style/gradient-container insets)}
:start {:x 0 :y 1} [quo/button
:end {:x 0 :y 0} {:style {:align-self :stretch
:style (style/gradient-container safe-area)} :margin-horizontal 20
[quo/button :margin-top 12}
{:style {:align-self :stretch :on-press #(on-press-confirm-selection selected)
:margin-horizontal 20} :accessibility-label :confirm-selection}
:on-press (fn [] (i18n/label :t/confirm-selection)]]))
(rf/dispatch [:bottom-sheet/hide])
(on-press-confirm-selection chat-id))
:accessibility-label :confirm-selection}
(i18n/label :t/confirm-selection)]])))])
(defn clear-button (defn clear-button
[] [selected]
(when (seq @selected) (when (seq @selected)
[rn/touchable-opacity [rn/touchable-opacity
{:on-press #(reset! selected []) {:on-press #(reset! selected [])
@ -52,66 +50,87 @@
(defn remove-selected (defn remove-selected
[coll item] [coll item]
(vec (remove #(= % item) coll))) (vec (remove #(= (:uri item) (:uri %)) coll)))
(defn image (defn image
[item index _ {:keys [window-width]}] [item index _ {:keys [window-width selected]}]
[rn/touchable-opacity [rn/touchable-opacity
{:active-opacity 1 {:active-opacity 1
:on-press (fn [] :on-press (fn []
(if (some #{item} @selected) (if (some #(= (:uri item) (:uri %)) @selected)
(swap! selected remove-selected item) (swap! selected remove-selected item)
(swap! selected conj item))) (if (>= (count @selected) constants/max-album-photos)
(rf/dispatch [:toasts/upsert
{:id :random-id
:icon :info
:icon-color colors/danger-50-opa-40
:container-style {:top (when platform/ios? 20)}
:text (i18n/label :t/only-6-images)}])
(swap! selected conj item))))
:accessibility-label (str "image-" index)} :accessibility-label (str "image-" index)}
[rn/image [rn/image
{:source {:uri (:uri item)} {:source {:uri (:uri item)}
:style (style/image window-width index)}] :style (style/image window-width index)}]
(when (some #{item} @selected) (when (some #(= (:uri item) (:uri %)) @selected)
[rn/view {:style (style/overlay window-width)}]) [rn/view {:style (style/overlay window-width)}])
(when (some #{item} @selected) (when (some #(= (:uri item) (:uri %)) @selected)
[info-count/info-count [info-count/info-count
{:style style/image-count {:style style/image-count
:accessibility-label (str "count-" index)} :accessibility-label (str "count-" index)}
(inc (utils/first-index #(= item %) @selected))])]) (inc (utils/first-index #(= (:uri item) (:uri %)) @selected))])])
(defn album-title
[photos? selected-album]
[rn/touchable-opacity
{:style (style/title-container)
:active-opacity 1
:accessibility-label :album-title
:on-press #(rf/dispatch (if photos?
[:open-modal :album-selector]
[:navigate-back]))}
[quo/text {:weight :medium} 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
[chat-id] []
(rf/dispatch [:chat.ui/camera-roll-get-photos 20]) [:f>
(let [selected-images (keys (rf/sub [:chats/sending-image]))] (fn []
[:f> (let [selected-images (rf/sub [:chats/sending-image])
(fn [] selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))
(rn/use-effect-once selected (reagent/atom [])]
(rn/use-effect
(fn [] (fn []
(rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album])
(if selected-images (if selected-images
(reset! selected (vec selected-images)) (reset! selected (vec (vals selected-images)))
(reset! selected [])) (reset! selected [])))
js/undefined)) [selected-album])
(let [{window-height :height window-width :width} (rn/use-window-dimensions) [safe-area/consumer
safe-area (safe-area/use-safe-area) (fn [insets]
camera-roll-photos (rf/sub [:camera-roll/photos]) (let [window-width (:width (rn/get-window))
end-cursor (rf/sub [:camera-roll/end-cursor]) camera-roll-photos (rf/sub [:camera-roll/photos])
loading? (rf/sub [:camera-roll/loading-more]) end-cursor (rf/sub [:camera-roll/end-cursor])
has-next-page? (rf/sub [:camera-roll/has-next-page])] loading? (rf/sub [:camera-roll/loading-more])
[rn/view {:style {:height (- window-height (:top safe-area))}} has-next-page? (rf/sub [:camera-roll/has-next-page])]
[rn/touchable-opacity [rn/view {:style {:flex 1}}
{:on-press #(js/alert "Camera: not implemented") [rn/view
:style (style/camera-button-container)} {:style style/buttons-container}
[quo/icon :i/camera {:color (colors/theme-colors colors/neutral-100 colors/white)}]] [album-title true selected-album]
[rn/view [clear-button selected]]
{:style style/title-container} [rn/flat-list
[quo/text {:weight :medium} (i18n/label :t/recent)] {:key-fn identity
[rn/view {:style (style/chevron-container)} :render-fn image
[quo/icon :i/chevron-down {:color (colors/theme-colors colors/neutral-100 colors/white)}]]] :render-data {:window-width window-width :selected selected}
[clear-button] :data camera-roll-photos
[rn/flat-list :num-columns 3
{:key-fn identity :content-container-style {:width "100%"
:render-fn image :padding-bottom (+ (:bottom insets) 100)
:render-data {:window-width window-width} :padding-top 80}
:data camera-roll-photos :on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
:num-columns 3 selected-album loading?
:content-container-style {:width "100%" has-next-page?])}]
:padding-bottom (+ (:bottom safe-area) 100)} [bottom-gradient selected-images insets selected]]))]))])
:style {:border-radius 20}
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor loading?
has-next-page?])}]
[bottom-gradient chat-id selected-images]]))]))

View File

@ -9,9 +9,12 @@
[status-im2.contexts.quo-preview.main :as quo.preview] [status-im2.contexts.quo-preview.main :as quo.preview]
[status-im2.contexts.shell.view :as shell] [status-im2.contexts.shell.view :as shell]
[status-im2.contexts.syncing.view :as settings-syncing] [status-im2.contexts.syncing.view :as settings-syncing]
[status-im2.contexts.chat.images-horizontal.view :as images-horizontal] [status-im2.contexts.chat.lightbox.view :as lightbox]
[status-im2.config :as config] [status-im2.config :as config]
[quo.design-system.colors :as colors])) [quo.design-system.colors :as colors]
[status-im2.contexts.chat.photo-selector.album-selector.view :as album-selector]
[react-native.platform :as platform]
[status-im2.contexts.chat.photo-selector.view :as photo-selector]))
(def components (def components
[]) [])
@ -32,7 +35,7 @@
:options {:topBar {:visible false}} :options {:topBar {:visible false}}
:component chat/chat} :component chat/chat}
{:name :images-horizontal {:name :lightbox
:insets {:top false :bottom false} :insets {:top false :bottom false}
:options {:topBar {:visible false} :options {:topBar {:visible false}
:statusBar {:backgroundColor colors/black-persist :statusBar {:backgroundColor colors/black-persist
@ -46,7 +49,15 @@
:toId :shared-element :toId :shared-element
:interpolation {:type :interpolation {:type
:decelerate}}]}}} :decelerate}}]}}}
:component images-horizontal/images-horizontal} :component lightbox/lightbox}
{:name :photo-selector
:options {:topBar {:visible false}}
:component photo-selector/photo-selector}
{:name :album-selector
:options {:topBar {:visible false}
:modalPresentationStyle (if platform/ios? :overCurrentContext :none)}
:component album-selector/album-selector}
{:name :new-contact {:name :new-contact
:options {:topBar {:visible false}} :options {:topBar {:visible false}}

View File

@ -110,23 +110,24 @@
(defn albumize-messages (defn albumize-messages
[messages] [messages]
(get (reduce (fn [{:keys [messages albums]} message] (get
(let [album-id (:album-id message) (reduce (fn [{:keys [messages albums]} message]
albums (cond-> albums album-id (update album-id conj message)) (let [album-id (:album-id message)
messages (if album-id albums (cond-> albums album-id (update album-id conj message))
(conj (filterv #(not= album-id (:album-id %)) messages) messages (if album-id
{:album (get albums album-id) (conj (filterv #(not= album-id (:album-id %)) messages)
:album-id album-id {:album (get albums album-id)
:albumize? (:albumize? message) :album-id album-id
:message-id album-id :albumize? (:albumize? message)
:content-type constants/content-type-album}) :message-id album-id
(conj messages message))] :content-type constants/content-type-album})
{:messages messages (conj messages message))]
:albums albums})) {:messages messages
{:messages [] :albums albums}))
:albums {}} {:messages []
messages) :albums {}}
:messages)) messages)
:messages))
(re-frame/reg-sub (re-frame/reg-sub
:chats/chat-messages :chats/chat-messages

View File

@ -109,6 +109,8 @@
(reg-root-key-sub :camera-roll/end-cursor :camera-roll/end-cursor) (reg-root-key-sub :camera-roll/end-cursor :camera-roll/end-cursor)
(reg-root-key-sub :camera-roll/has-next-page :camera-roll/has-next-page) (reg-root-key-sub :camera-roll/has-next-page :camera-roll/has-next-page)
(reg-root-key-sub :camera-roll/loading-more :camera-roll/loading-more) (reg-root-key-sub :camera-roll/loading-more :camera-roll/loading-more)
(reg-root-key-sub :camera-roll/albums :camera-roll/albums)
(reg-root-key-sub :camera-roll/selected-album :camera-roll/selected-album)
(reg-root-key-sub :group-chat/invitations :group-chat/invitations) (reg-root-key-sub :group-chat/invitations :group-chat/invitations)
(reg-root-key-sub :chats/mention-suggestions :chats/mention-suggestions) (reg-root-key-sub :chats/mention-suggestions :chats/mention-suggestions)
(reg-root-key-sub :chat/inputs-with-mentions :chat/inputs-with-mentions) (reg-root-key-sub :chat/inputs-with-mentions :chat/inputs-with-mentions)

View File

@ -1956,5 +1956,8 @@
"wants-to-join": "wants to join", "wants-to-join": "wants to join",
"connect-with-users": "Connect with users", "connect-with-users": "Connect with users",
"invite-friends-and-family": "Invite your friends and family to Status", "invite-friends-and-family": "Invite your friends and family to Status",
"you-have-no-contacts": "You have no contacts" "you-have-no-contacts": "You have no contacts",
"my-albums": "My albums",
"images": "images",
"only-6-images": "You can only add 6 images to your message"
} }