Skip to content

Commit 521ea88

Browse files
committed
feat: add video recording support for android
1 parent 9d02d86 commit 521ea88

File tree

6 files changed

+137
-95
lines changed

6 files changed

+137
-95
lines changed

package/expo-package/src/optionalDependencies/takePhoto.ts

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ type Size = {
1919
width?: number;
2020
};
2121

22+
// Media type mapping for iOS and Android
23+
const mediaTypeMap = {
24+
mixed: ['images', 'videos'],
25+
image: 'images',
26+
video: 'videos',
27+
};
28+
2229
export const takePhoto = ImagePicker
23-
? async ({ compressImageQuality = 1 }) => {
30+
? async ({ compressImageQuality = 1, mediaType = Platform.OS === 'ios' ? 'mixed' : 'image' }) => {
2431
try {
2532
const permissionCheck = await ImagePicker.getCameraPermissionsAsync();
2633
const canRequest = permissionCheck.canAskAgain;
@@ -36,61 +43,64 @@ export const takePhoto = ImagePicker
3643

3744
if (permissionGranted) {
3845
const imagePickerSuccessResult = await ImagePicker.launchCameraAsync({
39-
mediaTypes: Platform.OS === 'ios' ? ['images', 'videos'] : 'images',
46+
mediaTypes: Platform.OS === 'ios' ? mediaTypeMap[mediaType] : mediaType[mediaType],
4047
quality: Math.min(Math.max(0, compressImageQuality), 1),
4148
});
4249
const canceled = imagePickerSuccessResult.canceled;
4350
const assets = imagePickerSuccessResult.assets;
4451
// since we only support single photo upload for now we will only be focusing on 0'th element.
4552
const photo = assets && assets[0];
46-
if (Platform.OS === 'ios') {
47-
if (photo.mimeType.includes('video')) {
48-
const clearFilter = new RegExp('[.:]', 'g');
49-
const date = new Date().toISOString().replace(clearFilter, '_');
50-
return {
51-
...photo,
52-
cancelled: false,
53-
duration: photo.duration,
54-
source: 'camera',
55-
name: 'video_recording_' + date + photo.uri.split('.').pop(),
56-
size: photo.fileSize,
57-
type: photo.mimeType,
58-
uri: photo.uri,
59-
};
60-
}
53+
console.log('photo', photo);
54+
if (canceled) {
55+
return { cancelled: true };
6156
}
62-
if (canceled === false && photo && photo.height && photo.width && photo.uri) {
63-
let size: Size = {};
64-
if (Platform.OS === 'android') {
65-
const getSize = (): Promise<Size> =>
66-
new Promise((resolve) => {
67-
Image.getSize(photo.uri, (width, height) => {
68-
resolve({ height, width });
69-
});
70-
});
71-
72-
try {
73-
const { height, width } = await getSize();
74-
size.height = height;
75-
size.width = width;
76-
} catch (e) {
77-
console.warn('Error get image size of picture caputred from camera ', e);
78-
}
79-
} else {
80-
size = {
81-
height: photo.height,
82-
width: photo.width,
83-
};
84-
}
85-
57+
if (photo.mimeType.includes('video')) {
58+
const clearFilter = new RegExp('[.:]', 'g');
59+
const date = new Date().toISOString().replace(clearFilter, '_');
8660
return {
61+
...photo,
8762
cancelled: false,
63+
duration: photo.duration, // in milliseconds
64+
source: 'camera',
65+
name: 'video_recording_' + date + photo.uri.split('.').pop(),
8866
size: photo.fileSize,
8967
type: photo.mimeType,
90-
source: 'camera',
9168
uri: photo.uri,
92-
...size,
9369
};
70+
} else {
71+
if (photo && photo.height && photo.width && photo.uri) {
72+
let size: Size = {};
73+
if (Platform.OS === 'android') {
74+
const getSize = (): Promise<Size> =>
75+
new Promise((resolve) => {
76+
Image.getSize(photo.uri, (width, height) => {
77+
resolve({ height, width });
78+
});
79+
});
80+
81+
try {
82+
const { height, width } = await getSize();
83+
size.height = height;
84+
size.width = width;
85+
} catch (e) {
86+
console.warn('Error get image size of picture caputred from camera ', e);
87+
}
88+
} else {
89+
size = {
90+
height: photo.height,
91+
width: photo.width,
92+
};
93+
}
94+
95+
return {
96+
cancelled: false,
97+
size: photo.fileSize,
98+
type: photo.mimeType,
99+
source: 'camera',
100+
uri: photo.uri,
101+
...size,
102+
};
103+
}
94104
}
95105
}
96106
} catch (error) {

package/native-package/src/optionalDependencies/takePhoto.ts

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ try {
1111
}
1212

1313
export const takePhoto = ImagePicker
14-
? async ({ compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1 }) => {
14+
? async ({
15+
compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1,
16+
mediaType = Platform.OS === 'ios' ? 'mixed' : 'image',
17+
}) => {
1518
if (Platform.OS === 'android') {
1619
const cameraPermissions = await PermissionsAndroid.check(
1720
PermissionsAndroid.PERMISSIONS.CAMERA,
@@ -29,7 +32,7 @@ export const takePhoto = ImagePicker
2932
}
3033
try {
3134
const result = await ImagePicker.launchCamera({
32-
mediaType: Platform.OS === 'ios' ? 'mixed' : 'images',
35+
mediaType: mediaType,
3336
quality: Math.min(Math.max(0, compressImageQuality), 1),
3437
});
3538
if (!result.assets.length) {
@@ -38,56 +41,55 @@ export const takePhoto = ImagePicker
3841
};
3942
}
4043
const asset = result.assets[0];
41-
if (Platform.OS === 'ios') {
42-
if (asset.type.includes('video')) {
43-
const clearFilter = new RegExp('[.:]', 'g');
44-
const date = new Date().toISOString().replace(clearFilter, '_');
44+
if (asset.type.includes('video')) {
45+
const clearFilter = new RegExp('[.:]', 'g');
46+
const date = new Date().toISOString().replace(clearFilter, '_');
47+
return {
48+
...asset,
49+
cancelled: false,
50+
duration: asset.duration * 1000,
51+
source: 'camera',
52+
name: 'video_recording_' + date + asset.fileName.split('.').pop(),
53+
size: asset.fileSize,
54+
type: asset.type,
55+
uri: asset.uri,
56+
};
57+
} else {
58+
if (asset.height && asset.width && asset.uri) {
59+
let size: { height?: number; width?: number } = {};
60+
if (Platform.OS === 'android') {
61+
// Height and width returned by ImagePicker are incorrect on Android.
62+
const getSize = (): Promise<{ height: number; width: number }> =>
63+
new Promise((resolve) => {
64+
Image.getSize(asset.uri, (width, height) => {
65+
resolve({ height, width });
66+
});
67+
});
68+
69+
try {
70+
const { height, width } = await getSize();
71+
size.height = height;
72+
size.width = width;
73+
} catch (e) {
74+
// do nothing
75+
console.warn('Error get image size of picture caputred from camera ', e);
76+
}
77+
} else {
78+
size = {
79+
height: asset.height,
80+
width: asset.width,
81+
};
82+
}
4583
return {
46-
...asset,
4784
cancelled: false,
48-
duration: asset.duration ? asset.duration * 1000 : undefined, // in milliseconds
49-
source: 'camera',
50-
name: 'video_recording_' + date + asset.fileName.split('.').pop(),
51-
size: asset.fileSize,
5285
type: asset.type,
86+
size: asset.size,
87+
source: 'camera',
5388
uri: asset.uri,
89+
...size,
5490
};
5591
}
5692
}
57-
if (asset.height && asset.width && asset.uri) {
58-
let size: { height?: number; width?: number } = {};
59-
if (Platform.OS === 'android') {
60-
// Height and width returned by ImagePicker are incorrect on Android.
61-
const getSize = (): Promise<{ height: number; width: number }> =>
62-
new Promise((resolve) => {
63-
Image.getSize(asset.uri, (width, height) => {
64-
resolve({ height, width });
65-
});
66-
});
67-
68-
try {
69-
const { height, width } = await getSize();
70-
size.height = height;
71-
size.width = width;
72-
} catch (e) {
73-
// do nothing
74-
console.warn('Error get image size of picture caputred from camera ', e);
75-
}
76-
} else {
77-
size = {
78-
height: asset.height,
79-
width: asset.width,
80-
};
81-
}
82-
return {
83-
cancelled: false,
84-
type: asset.type,
85-
size: asset.size,
86-
source: 'camera',
87-
uri: asset.uri,
88-
...size,
89-
};
90-
}
9193
} catch (e: unknown) {
9294
if (e instanceof Error) {
9395
// on iOS: if it was in inactive state, then the user had just denied the permissions

package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useMessageInputContext } from '../../../contexts/messageInputContext/Me
77
import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
88
import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
99
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
10+
import { Recorder } from '../../../icons';
1011

1112
const styles = StyleSheet.create({
1213
container: {
@@ -48,6 +49,7 @@ export const AttachmentPickerSelectionBar = () => {
4849
const {
4950
theme: {
5051
attachmentSelectionBar: { container, icon },
52+
colors: { grey },
5153
},
5254
} = useTheme();
5355

@@ -105,7 +107,9 @@ export const AttachmentPickerSelectionBar = () => {
105107
{hasCameraPicker ? (
106108
<TouchableOpacity
107109
hitSlop={{ bottom: 15, top: 15 }}
108-
onPress={takeAndUploadImage}
110+
onPress={() => {
111+
takeAndUploadImage();
112+
}}
109113
testID='take-photo-touchable'
110114
>
111115
<View style={[styles.icon, icon]}>
@@ -116,6 +120,19 @@ export const AttachmentPickerSelectionBar = () => {
116120
</View>
117121
</TouchableOpacity>
118122
) : null}
123+
{hasCameraPicker ? (
124+
<TouchableOpacity
125+
hitSlop={{ bottom: 15, top: 15 }}
126+
onPress={() => {
127+
takeAndUploadImage('video');
128+
}}
129+
testID='take-photo-touchable'
130+
>
131+
<View style={[styles.icon, icon]}>
132+
<Recorder pathFill={grey} height={20} width={20} />
133+
</View>
134+
</TouchableOpacity>
135+
) : null}
119136
{!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads
120137
<TouchableOpacity
121138
hitSlop={{ bottom: 15, top: 15 }}

package/src/components/MessageInput/components/NativeAttachmentPicker.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSele
1313
import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon';
1414
import { ImageSelectorIcon } from '../../AttachmentPicker/components/ImageSelectorIcon';
1515
import { CreatePollIcon } from '../../Poll/components/CreatePollIcon';
16+
import { Recorder } from '../../../icons';
1617

1718
type NativeAttachmentPickerProps = {
1819
onRequestedClose: () => void;
@@ -28,10 +29,10 @@ export const NativeAttachmentPicker = ({
2829
}: NativeAttachmentPickerProps) => {
2930
const size = attachButtonLayoutRectangle?.width ?? 0;
3031
const attachButtonItemSize = 40;
31-
const NUMBER_OF_BUTTONS = 3;
32+
const NUMBER_OF_BUTTONS = 5;
3233
const {
3334
theme: {
34-
colors: { grey_whisper },
35+
colors: { grey, grey_whisper },
3536
messageInput: {
3637
nativeAttachmentPicker: {
3738
buttonContainer,
@@ -149,6 +150,13 @@ export const NativeAttachmentPicker = ({
149150
id: 'Camera',
150151
onPressHandler: takeAndUploadImage,
151152
});
153+
buttons.push({
154+
icon: <Recorder pathFill={grey} height={20} width={20} />,
155+
id: 'Video',
156+
onPressHandler: () => {
157+
takeAndUploadImage('video');
158+
},
159+
});
152160
}
153161

154162
return (

package/src/contexts/messageInputContext/MessageInputContext.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import type { Emoji } from '../../emoji-data';
5757
import {
5858
isDocumentPickerAvailable,
5959
isImageMediaLibraryAvailable,
60+
MediaTypes,
6061
pickDocument,
6162
pickImage,
6263
takePhoto,
@@ -232,7 +233,7 @@ export type LocalMessageInputContext<
232233
/**
233234
* Function for taking a photo and uploading it
234235
*/
235-
takeAndUploadImage: () => Promise<void>;
236+
takeAndUploadImage: (mediaType?: MediaTypes) => Promise<void>;
236237
text: string;
237238
toggleAttachmentPicker: () => void;
238239
/**
@@ -686,10 +687,10 @@ export const MessageInputProvider = <
686687
/**
687688
* Function for capturing a photo and uploading it
688689
*/
689-
const takeAndUploadImage = async () => {
690+
const takeAndUploadImage = async (mediaType?: MediaTypes) => {
690691
setSelectedPicker(undefined);
691692
closePicker();
692-
const photo = await takePhoto({ compressImageQuality: value.compressImageQuality });
693+
const photo = await takePhoto({ compressImageQuality: value.compressImageQuality, mediaType });
693694
if (photo.askToOpenSettings) {
694695
Alert.alert(
695696
t('Allow camera access in device settings'),

package/src/native.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ type Photo = Omit<Asset, 'source'> & {
8585
askToOpenSettings?: boolean;
8686
cancelled?: boolean;
8787
};
88-
type TakePhoto = (options: { compressImageQuality?: number }) => Promise<Photo> | never;
88+
export type MediaTypes = 'image' | 'video' | 'mixed';
89+
type TakePhoto = (options: {
90+
compressImageQuality?: number;
91+
mediaType?: MediaTypes;
92+
}) => Promise<Photo> | never;
8993
export let takePhoto: TakePhoto = fail;
9094

9195
type HapticFeedbackMethod =

0 commit comments

Comments
 (0)