Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
jest.mock(
'expo-media-library',
() => ({
MediaType: {
photo: 'photo',
video: 'video',
},
SortBy: {
modificationTime: 'modificationTime',
},
getAssetsAsync: jest.fn(),
getPermissionsAsync: jest.fn(),
requestPermissionsAsync: jest.fn(),
}),
{ virtual: true },
);

jest.mock('../getLocalAssetUri', () => ({
getLocalAssetUri: jest.fn(),
}));

import * as MediaLibrary from 'expo-media-library';

import { getLocalAssetUri } from '../getLocalAssetUri';
import { getPhotos } from '../getPhotos';

const mockedMediaLibrary = MediaLibrary as {
getAssetsAsync: jest.Mock;
getPermissionsAsync: jest.Mock;
requestPermissionsAsync: jest.Mock;
};

const mockedGetLocalAssetUri = getLocalAssetUri as jest.Mock;

describe('getPhotos', () => {
beforeEach(() => {
mockedMediaLibrary.getPermissionsAsync.mockResolvedValue({
accessPrivileges: 'all',
status: 'granted',
});
mockedMediaLibrary.requestPermissionsAsync.mockResolvedValue({
status: 'granted',
});
mockedMediaLibrary.getAssetsAsync.mockReset();
mockedGetLocalAssetUri.mockReset();
mockedGetLocalAssetUri.mockResolvedValue(undefined);
});

it('falls back to media-type mime strings when filename mime detection returns null', async () => {
mockedMediaLibrary.getAssetsAsync.mockResolvedValue({
assets: [
{
duration: 0,
filename: 'IMG_0001',
height: 100,
id: 'photo-1',
mediaType: MediaLibrary.MediaType.photo,
uri: 'ph://photo-1',
width: 200,
},
{
duration: 12,
filename: 'VID_0002',
height: 300,
id: 'video-1',
mediaType: MediaLibrary.MediaType.video,
uri: 'ph://video-1',
width: 400,
},
],
endCursor: undefined,
hasNextPage: false,
});

const result = await getPhotos({ after: undefined, first: 20 });

expect(result.assets).toEqual([
expect.objectContaining({
type: 'image/*',
}),
expect.objectContaining({
type: 'video/*',
}),
]);
});
});
5 changes: 4 additions & 1 deletion package/expo-package/src/optionalDependencies/getPhotos.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Platform } from 'react-native';

import mime from 'mime';

import type { File } from 'stream-chat-react-native-core';
Expand Down Expand Up @@ -54,7 +55,9 @@ export const getPhotos = MediaLibrary
const assets = await Promise.all(
results.assets.map(async (asset) => {
const localUri = await getLocalAssetUri(asset.id);
const mimeType = mime.getType(asset.filename);
const mimeType =
mime.getType(asset.filename || asset.uri) ||
(asset.mediaType === MediaLibrary.MediaType.video ? 'video/*' : 'image/*');
return {
duration: asset.duration * 1000,
height: asset.height,
Expand Down
12 changes: 10 additions & 2 deletions package/expo-package/src/optionalDependencies/pickDocument.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import mime from 'mime';

let DocumentPicker;

try {
Expand Down Expand Up @@ -40,7 +42,10 @@ export const pickDocument = DocumentPicker
return {
assets: assets.map((asset) => ({
...asset,
type: asset.mimeType,
type:
asset.mimeType ||
mime.getType(asset.name || asset.uri) ||
'application/octet-stream',
})),
cancelled: false,
};
Expand All @@ -50,7 +55,10 @@ export const pickDocument = DocumentPicker
assets: [
{
...rest,
type: rest.mimeType,
type:
rest.mimeType ||
mime.getType(rest.name || rest.uri) ||
'application/octet-stream',
},
],
cancelled: false,
Expand Down
6 changes: 5 additions & 1 deletion package/expo-package/src/optionalDependencies/pickImage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Platform } from 'react-native';
import mime from 'mime';
import { PickImageOptions } from 'stream-chat-react-native-core';
let ImagePicker;

Expand Down Expand Up @@ -47,7 +48,10 @@ export const pickImage = ImagePicker
duration: asset.duration,
name: asset.fileName,
size: asset.fileSize,
type: asset.mimeType,
type:
asset.mimeType ||
mime.getType(asset.fileName || asset.uri) ||
(asset.duration ? 'video/*' : 'image/*'),
uri: asset.uri,
}));
return { assets, cancelled: false };
Expand Down
10 changes: 7 additions & 3 deletions package/expo-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Image, Platform } from 'react-native';

import mime from 'mime';

let ImagePicker;

try {
Expand Down Expand Up @@ -54,7 +56,9 @@ export const takePhoto = ImagePicker
if (!photo) {
return { cancelled: true };
}
if (photo.mimeType.includes('video')) {
const mimeType =
photo.mimeType || mime.getType(photo.uri) || (photo.duration ? 'video/*' : 'image/*');
if (mimeType.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
Expand All @@ -63,7 +67,7 @@ export const takePhoto = ImagePicker
duration: photo.duration, // in milliseconds
name: 'video_recording_' + date + '.' + photo.uri.split('.').pop(),
size: photo.fileSize,
type: photo.mimeType,
type: mimeType,
uri: photo.uri,
};
} else {
Expand Down Expand Up @@ -96,7 +100,7 @@ export const takePhoto = ImagePicker
cancelled: false,
name: 'image_' + date + '.' + photo.uri.split('.').pop(),
size: photo.fileSize,
type: photo.mimeType,
type: mimeType,
uri: photo.uri,
...size,
};
Expand Down
9 changes: 6 additions & 3 deletions package/native-package/src/optionalDependencies/getPhotos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,12 @@ export const getPhotos = CameraRollDependency
results.edges.map(async (edge) => {
const originalUri = edge.node?.image?.uri;
const type =
Platform.OS === 'ios'
? mime.getType(edge.node.image.filename as string)
: edge.node.type;
(Platform.OS === 'ios'
? mime.getType(edge.node.image.filename as string) || edge.node.type
: edge.node.type) ||
mime.getType(edge.node.image.filename as string) ||
mime.getType(originalUri) ||
(edge.node.image.playableDuration ? 'video/*' : 'image/*');
const isImage = type.includes('image');

const uri =
Expand Down
6 changes: 5 additions & 1 deletion package/native-package/src/optionalDependencies/pickImage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Platform } from 'react-native';
import mime from 'mime';
import { PickImageOptions } from 'stream-chat-react-native-core';
let ImagePicker;

Expand Down Expand Up @@ -28,7 +29,10 @@ export const pickImage = ImagePicker
duration: asset.duration ? asset.duration * 1000 : undefined, // in milliseconds
name: asset.fileName,
size: asset.fileSize,
type: asset.type,
type:
asset.type ||
mime.getType(asset.fileName || asset.uri) ||
(asset.duration ? 'video/*' : 'image/*'),
uri: asset.uri,
}));
return { assets, cancelled: false };
Expand Down
11 changes: 8 additions & 3 deletions package/native-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppState, Image, PermissionsAndroid, Platform } from 'react-native';
import mime from 'mime';

let ImagePicker;

Expand Down Expand Up @@ -46,7 +47,11 @@ export const takePhoto = ImagePicker
cancelled: true,
};
}
if (asset.type.includes('video')) {
const assetType =
asset.type ||
mime.getType(asset.fileName || asset.uri) ||
(mediaType === 'video' || asset.duration ? 'video/*' : 'image/*');
if (assetType.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
Expand All @@ -55,7 +60,7 @@ export const takePhoto = ImagePicker
duration: asset.duration * 1000,
name: 'video_recording_' + date + '.' + asset.fileName.split('.').pop(),
size: asset.fileSize,
type: asset.type,
type: assetType,
uri: asset.uri,
};
} else {
Expand Down Expand Up @@ -90,7 +95,7 @@ export const takePhoto = ImagePicker
cancelled: false,
name: 'image_' + date + '.' + asset.uri.split('.').pop(),
size: asset.fileSize,
type: asset.type,
type: assetType,
uri: asset.uri,
...size,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const renderAttachmentPickerItem = ({ item }: { item: AttachmentPickerIte
* Native iOS - Gives `image` or `video`
* Expo Android/iOS - Gives `photo` or `video`
**/
const isVideoType = asset.type.includes('video');
const isVideoType = asset.type?.includes('video');

if (isVideoType) {
return (
Expand Down
24 changes: 19 additions & 5 deletions package/src/contexts/messageInputContext/MessageInputContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
import { Alert, Keyboard, Linking, TextInput, TextInputProps } from 'react-native';

import { BottomSheetHandleProps } from '@gorhom/bottom-sheet';
import { lookup as lookupMimeType } from 'mime-types';
import {
LocalMessage,
MessageComposer,
Expand Down Expand Up @@ -652,13 +653,26 @@ export const MessageInputProvider = ({

const uploadNewFile = useStableCallback(async (file: File) => {
try {
if (!file?.uri) {
return;
}

const fallbackMimeType =
lookupMimeType(file.name || file.uri || '') ||
(file.duration ? 'video/*' : file.height && file.width ? 'image/*' : undefined);
const normalizedFile = {
...file,
type:
file.type ||
(typeof fallbackMimeType === 'string' ? fallbackMimeType : 'application/octet-stream'),
};
uploadAbortControllerRef.current.set(file.name, client.createAbortControllerForNextRequest());
const fileURI = file.type.includes('image')
? await compressedImageURI(file, value.compressImageQuality)
: file.uri;
const updatedFile = { ...file, uri: fileURI };
const fileURI = normalizedFile.type.includes('image')
? await compressedImageURI(normalizedFile, value.compressImageQuality)
: normalizedFile.uri;
const updatedFile = { ...normalizedFile, uri: fileURI };
await attachmentManager.uploadFiles([updatedFile]);
uploadAbortControllerRef.current.delete(file.name);
uploadAbortControllerRef.current.delete(normalizedFile.name);
} catch (error) {
if (
error instanceof Error &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,47 @@ describe("MessageInputContext's pickAndUploadImageFromNativePicker", () => {
});
},
);

it('does not crash when pickImage returns an asset with a null mime type', async () => {
const { attachmentManager } = channel.messageComposer;
jest.spyOn(NativeHandlers, 'pickImage').mockImplementation(
jest.fn().mockResolvedValue({
assets: [
{
duration: 0,
height: 100,
name: 'IMG_0001',
size: 123,
type: null,
uri: 'file:///tmp/IMG_0001',
width: 200,
},
],
cancelled: false,
}),
);

jest.spyOn(attachmentManager, 'availableUploadSlots', 'get').mockReturnValue(2);

const { result } = renderHook(() => useMessageInputContext(), {
initialProps,
wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
});

const uploadFilesSpy = jest.spyOn(attachmentManager, 'uploadFiles');

await waitFor(() => {
result.current.pickAndUploadImageFromNativePicker();
});

await waitFor(() => {
expect(uploadFilesSpy).toHaveBeenCalledWith([
expect.objectContaining({
type: 'image/*',
}),
]);
});
});
});

describe("MessageInputContext's takeAndUploadImage", () => {
Expand Down
Loading