@@ -17,6 +17,7 @@ import { IconImage, IconCamera, IconAttach, IconVideo } from '../common/Icons';
1717import { androidEnsureStoragePermission } from '../lightbox/download' ;
1818import { ThemeContext } from '../styles/theme' ;
1919import type { SpecificIconType } from '../common/Icons' ;
20+ import { androidSdkVersion } from '../reactNativeUtils' ;
2021
2122export type Attachment = { |
2223 + name : string | null ,
@@ -72,6 +73,21 @@ export const chooseUploadImageFilename = (uri: string, fileName: string): string
7273 return nameWithoutPrefix ;
7374} ;
7475
76+ // From the doc:
77+ // https://github.com/react-native-image-picker/react-native-image-picker/tree/v4.10.2#options
78+ // > Only iOS version >= 14 & Android version >= 13 support [multi-select]
79+ //
80+ // Older versions of react-native-image-picker claim to support multi-select
81+ // on older Android versions; we tried that and it gave a bad experience,
82+ // like a generic file picker that wasn't dedicated to handling images well:
83+ // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/Android.20select.20multiple.20photos/near/1423109
84+ //
85+ // But multi-select on iOS and on Android 13+ seem to work well.
86+ const kShouldOfferImageMultiselect =
87+ Platform . OS === 'ios'
88+ // Android 13
89+ || androidSdkVersion ( ) >= 33 ;
90+
7591type MenuButtonProps = $ReadOnly < { |
7692 onPress : ( ) => void | Promise < void > ,
7793 IconComponent : SpecificIconType ,
@@ -142,13 +158,13 @@ export default function ComposeMenu(props: Props): Node {
142158 return ;
143159 }
144160
161+ // This will have length one for single-select payloads, or more than
162+ // one for multi-select. So we'll treat `assets` uniformly: expect it
163+ // to have length >= 1, and loop over it, even if that means just one
164+ // iteration.
145165 const { assets } = response ;
146166
147- // TODO: support sending multiple files; see library's docs for how to
148- // let `assets` have more than one item in `response`.
149- const firstAsset = assets && assets [ 0 ] ;
150-
151- if ( ! firstAsset ) {
167+ if ( ! assets || ! assets [ 0 ] ) {
152168 // TODO: See if we these unexpected situations actually happen. …Ah,
153169 // yep, reportedly (and we've seen in Sentry):
154170 // https://github.com/react-native-image-picker/react-native-image-picker/issues/1945
@@ -159,22 +175,39 @@ export default function ComposeMenu(props: Props): Node {
159175 return ;
160176 }
161177
162- const { uri, fileName } = firstAsset ;
178+ const attachments = [ ] ;
179+ let numMalformed = 0 ;
180+ assets . forEach ( ( asset , i ) => {
181+ const { uri, fileName } = asset ;
163182
164- if ( uri == null || fileName == null ) {
165- // TODO: See if these unexpected situations actually happen.
166- showErrorAlert ( _ ( 'Error' ) , _ ( 'Failed to attach your file.' ) ) ;
167- logging . error (
168- 'First (should be only) asset returned from image picker had nullish `url` and/or `fileName`' ,
169- {
183+ if ( uri == null || fileName == null ) {
184+ // TODO: See if these unexpected situations actually happen.
185+ logging . error ( 'An asset returned from image picker had nullish `url` and/or `fileName`' , {
170186 'uri == null' : uri == null ,
171187 'fileName == null' : fileName == null ,
172- } ,
173- ) ;
174- return ;
188+ i,
189+ } ) ;
190+ numMalformed ++ ;
191+ return ;
192+ }
193+
194+ attachments . push ( { name : chooseUploadImageFilename ( uri , fileName ) , url : uri } ) ;
195+ } ) ;
196+
197+ if ( numMalformed > 0 ) {
198+ if ( assets . length === 1 && numMalformed === 1 ) {
199+ showErrorAlert ( _ ( 'Error' ) , _ ( 'Failed to attach your file.' ) ) ;
200+ return ;
201+ } else if ( assets . length === numMalformed ) {
202+ showErrorAlert ( _ ( 'Error' ) , _ ( 'Failed to attach your files.' ) ) ;
203+ return ;
204+ } else {
205+ showErrorAlert ( _ ( 'Error' ) , _ ( 'Failed to attach some of your files.' ) ) ;
206+ // no return; `attachments` will have some items that we can insert
207+ }
175208 }
176209
177- insertAttachments ( [ { name : chooseUploadImageFilename ( uri , fileName ) , url : uri } ] ) ;
210+ insertAttachments ( attachments ) ;
178211 } ,
179212 [ _ , insertAttachments ] ,
180213 ) ;
@@ -187,6 +220,14 @@ export default function ComposeMenu(props: Props): Node {
187220
188221 quality : 1.0 ,
189222 includeBase64 : false ,
223+
224+ // From the doc: "[U]se `0` to allow any number of files"
225+ // https://github.com/react-native-image-picker/react-native-image-picker/tree/v4.10.2#options
226+ //
227+ // Between single- and multi-select, we expect the payload passed to
228+ // handleImagePickerResponse to differ only in the length of the
229+ // `assets` array (one item vs. multiple).
230+ selectionLimit : kShouldOfferImageMultiselect ? 0 : 1 ,
190231 } ,
191232 handleImagePickerResponse ,
192233 ) ;
0 commit comments