Skip to content

Commit 32f3fc9

Browse files
compose: Allow image multi-selection, where supported
We recently completed the feature of not immediately sending messages with image uploads, in #5474. With that, it became possible to send a message with multiple uploaded images in it. But you had to go through the image-selection flow once for each of your chosen images; see discussion: #5474 (review) This gives a smoother experience by letting you choose multiple images in one image-selection session. Related: #2366 Co-authored-by: Akash Dhiman <[email protected]>
1 parent fbf3681 commit 32f3fc9

File tree

2 files changed

+59
-16
lines changed

2 files changed

+59
-16
lines changed

src/compose/ComposeMenu.js

+57-16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { IconImage, IconCamera, IconAttach, IconVideo } from '../common/Icons';
1717
import { androidEnsureStoragePermission } from '../lightbox/download';
1818
import { ThemeContext } from '../styles/theme';
1919
import type { SpecificIconType } from '../common/Icons';
20+
import { androidSdkVersion } from '../reactNativeUtils';
2021

2122
export 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+
7591
type 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
);

static/translations/messages_en.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"No one has read this message yet.": "No one has read this message yet.",
2525
"Confirm": "Confirm",
2626
"Failed to attach your file.": "Failed to attach your file.",
27+
"Failed to attach your files.": "Failed to attach your files.",
28+
"Failed to attach some of your files.": "Failed to attach some of your files.",
2729
"Configure permissions": "Configure permissions",
2830
"You": "You",
2931
"Discard changes": "Discard changes",

0 commit comments

Comments
 (0)