Skip to content

Commit 8411ebe

Browse files
committed
feat: Add media file size validation for images and videos and remove allowsEditing from image picker configurations.
1 parent 733b66d commit 8411ebe

2 files changed

Lines changed: 82 additions & 15 deletions

File tree

src/__tests__/components/TweetComposer.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ jest.mock('expo-image-picker', () => ({
4141
},
4242
}));
4343

44+
jest.mock('expo-file-system', () => ({
45+
getInfoAsync: jest.fn().mockResolvedValue({ exists: true, size: 1024 * 1024 }), // 1MB mock
46+
}));
47+
4448
jest.mock('expo-video', () => {
4549
const addListener = jest.fn(() => ({ remove: jest.fn() }));
4650
const React = jest.requireActual('react');
@@ -280,7 +284,6 @@ describe('TweetComposer', () => {
280284
mediaTypes: ['images', 'videos'],
281285
allowsMultipleSelection: true,
282286
selectionLimit: 4,
283-
allowsEditing: true,
284287
quality: 1,
285288
videoMaxDuration: 140,
286289
exif: false,
@@ -558,7 +561,6 @@ describe('TweetComposer', () => {
558561
expect(mockLaunchCamera).toHaveBeenCalledWith({
559562
mediaTypes: ['images'],
560563
quality: 1,
561-
allowsEditing: true,
562564
exif: false,
563565
});
564566
});

src/components/ui/TweetComposer.tsx

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616

1717
import { Ionicons } from '@expo/vector-icons';
1818
import { useQueryClient } from '@tanstack/react-query';
19+
import * as FileSystem from 'expo-file-system/legacy';
1920
import * as ImagePicker from 'expo-image-picker';
2021
import { replaceTriggerValues } from 'react-native-controlled-mentions';
2122
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -67,6 +68,26 @@ type TweetComposerProps = {
6768
const CHARACTER_LIMIT = 280;
6869
const MAX_CHARACTER_LIMIT = Math.ceil(CHARACTER_LIMIT / 100) * 100;
6970

71+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
72+
const MAX_VIDEO_SIZE = 10 * 1024 * 1024;
73+
74+
const checkFileSize = async (
75+
uri: string,
76+
isVideo: boolean
77+
): Promise<{ valid: boolean; sizeMB?: number }> => {
78+
try {
79+
const info = await FileSystem.getInfoAsync(uri);
80+
if (!info.exists || info.size === undefined || info.size === null) {
81+
return { valid: true };
82+
}
83+
const maxSize = isVideo ? MAX_VIDEO_SIZE : MAX_IMAGE_SIZE;
84+
const sizeMB = info.size / (1024 * 1024);
85+
return { valid: info.size <= maxSize, sizeMB };
86+
} catch {
87+
return { valid: true };
88+
}
89+
};
90+
7091
export default function TweetComposer({
7192
visible,
7293
onClose,
@@ -138,28 +159,52 @@ export default function TweetComposer({
138159
});
139160

140161
if (!result.canceled) {
141-
const newAssets: Asset[] = result.assets.map((asset) => ({
142-
id: asset.fileName || asset.uri,
143-
uri: asset.uri,
144-
fileName: asset.fileName ?? undefined,
145-
mediaType: asset.duration ? 'video' : 'photo',
146-
duration: asset.duration ? asset.duration / 1000 : undefined,
147-
}));
162+
const oversizedFiles: string[] = [];
163+
const validAssets: Asset[] = [];
164+
165+
for (const asset of result.assets) {
166+
const isVideo = !!asset.duration;
167+
const sizeCheck = await checkFileSize(asset.uri, isVideo);
168+
169+
if (!sizeCheck.valid) {
170+
const maxMB = isVideo ? 10 : 5;
171+
oversizedFiles.push(
172+
`${asset.fileName || 'File'} (${sizeCheck.sizeMB?.toFixed(1)}MB > ${maxMB}MB)`
173+
);
174+
} else {
175+
validAssets.push({
176+
id: asset.fileName || asset.uri,
177+
uri: asset.uri,
178+
fileName: asset.fileName ?? undefined,
179+
mediaType: isVideo ? 'video' : 'photo',
180+
duration: asset.duration ? asset.duration / 1000 : undefined,
181+
});
182+
}
183+
}
184+
185+
if (oversizedFiles.length > 0) {
186+
Alert.alert(
187+
'File Too Large',
188+
`The following files exceed size limits:\n${oversizedFiles.join('\n')}`
189+
);
190+
}
148191

149192
const availableSlots = 4 - selectedAssets.length;
150-
const assetsToAdd = newAssets
193+
const assetsToAdd = validAssets
151194
.filter(
152195
(asset) =>
153196
!selectedAssets.some((a) => (a.fileName || a.id) === (asset.fileName || asset.id))
154197
)
155198
.slice(0, availableSlots);
156199

157-
setSelectedAssets([...assetsToAdd, ...selectedAssets]);
200+
if (assetsToAdd.length > 0) {
201+
setSelectedAssets([...assetsToAdd, ...selectedAssets]);
158202

159-
const newForAllPicked = assetsToAdd.filter(
160-
(asset) => !allPickedAssets.some((a) => a.fileName === asset.fileName)
161-
);
162-
setAllPickedAssets([...allPickedAssets, ...newForAllPicked]);
203+
const newForAllPicked = assetsToAdd.filter(
204+
(asset) => !allPickedAssets.some((a) => a.fileName === asset.fileName)
205+
);
206+
setAllPickedAssets([...allPickedAssets, ...newForAllPicked]);
207+
}
163208
}
164209
} finally {
165210
setIsPickingMedia(false);
@@ -283,6 +328,16 @@ export default function TweetComposer({
283328

284329
if (!result.canceled) {
285330
const asset = result.assets[0];
331+
const sizeCheck = await checkFileSize(asset.uri, false);
332+
333+
if (!sizeCheck.valid) {
334+
Alert.alert(
335+
'Image Too Large',
336+
`This image is ${sizeCheck.sizeMB?.toFixed(1)}MB. Maximum size is 5MB.`
337+
);
338+
return;
339+
}
340+
286341
const newAsset: Asset = {
287342
id: asset.uri,
288343
uri: asset.uri,
@@ -317,6 +372,16 @@ export default function TweetComposer({
317372

318373
if (!result.canceled) {
319374
const asset = result.assets[0];
375+
const sizeCheck = await checkFileSize(asset.uri, true);
376+
377+
if (!sizeCheck.valid) {
378+
Alert.alert(
379+
'Video Too Large',
380+
`This video is ${sizeCheck.sizeMB?.toFixed(1)}MB. Maximum size is 10MB.`
381+
);
382+
return;
383+
}
384+
320385
const newAsset: Asset = {
321386
id: asset.uri,
322387
uri: asset.uri,

0 commit comments

Comments
 (0)