Skip to content

Commit

Permalink
Merge pull request #2898 from GetStream/develop
Browse files Browse the repository at this point in the history
Next Release
  • Loading branch information
isekovanic authored Jan 17, 2025
2 parents 261f8a6 + 020cecc commit 1113ce1
Show file tree
Hide file tree
Showing 27 changed files with 561 additions and 355 deletions.
3 changes: 2 additions & 1 deletion examples/ExpoMessaging/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let them share with others.",
"cameraPermission": "The app accesses your camera to let you take photos and share with others."
"cameraPermission": "The app accesses your camera to let you take photos and share with others.",
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone for video recording."
}
],
[
Expand Down
8 changes: 7 additions & 1 deletion examples/ExpoMessaging/components/ChatWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AuthProgressLoader } from './AuthProgressLoader';
import { StreamChatGenerics } from '../types';
import { STREAM_API_KEY, user, userToken } from '../constants';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useStreamChatTheme } from '../useStreamChatTheme';

const streami18n = new Streami18n({
language: 'en',
Expand All @@ -26,13 +27,18 @@ export const ChatWrapper = ({ children }: PropsWithChildren<{}>) => {
userData: user,
tokenOrProvider: userToken,
});
const theme = useStreamChatTheme();

if (!chatClient) {
return <AuthProgressLoader />;
}

return (
<OverlayProvider<StreamChatGenerics> bottomInset={bottom} i18nInstance={streami18n}>
<OverlayProvider<StreamChatGenerics>
bottomInset={bottom}
i18nInstance={streami18n}
value={{ style: theme }}
>
<Chat client={chatClient} i18nInstance={streami18n}>
{children}
</Chat>
Expand Down
13 changes: 7 additions & 6 deletions examples/ExpoMessaging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,28 @@
"@op-engineering/op-sqlite": "^9.3.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/elements": "^1.3.30",
"expo": "^52.0.0",
"expo": "~52.0.20",
"expo-av": "~15.0.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.3",
"expo-document-picker": "~13.0.1",
"expo-file-system": "~18.0.4",
"expo-file-system": "~18.0.6",
"expo-haptics": "~14.0.0",
"expo-image-manipulator": "~13.0.5",
"expo-image-picker": "^16.0.3",
"expo-linking": "~7.0.3",
"expo-router": "~4.0.11",
"expo-media-library": "~17.0.4",
"expo-router": "~4.0.15",
"expo-sharing": "~13.0.0",
"expo-splash-screen": "~0.29.13",
"expo-splash-screen": "~0.29.18",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.3",
"react-native": "0.76.5",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"stream-chat-expo": "link:../../package/expo-package",
Expand Down
390 changes: 219 additions & 171 deletions examples/ExpoMessaging/yarn.lock

Large diffs are not rendered by default.

91 changes: 59 additions & 32 deletions package/expo-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ try {

if (!ImagePicker) {
console.log(
'expo-image-picker is not installed. Installing this package will enable campturing photos through the app, and thereby send it.',
'expo-image-picker is not installed. Installing this package will enable capturing photos and videos(for iOS) through the app, and thereby send it.',
);
}

Expand All @@ -19,8 +19,15 @@ type Size = {
width?: number;
};

// Media type mapping for iOS and Android
const mediaTypeMap = {
image: 'images',
mixed: ['images', 'videos'],
video: 'videos',
};

export const takePhoto = ImagePicker
? async ({ compressImageQuality = 1 }) => {
? async ({ compressImageQuality = 1, mediaType = Platform.OS === 'ios' ? 'mixed' : 'image' }) => {
try {
const permissionCheck = await ImagePicker.getCameraPermissionsAsync();
const canRequest = permissionCheck.canAskAgain;
Expand All @@ -35,45 +42,65 @@ export const takePhoto = ImagePicker
}

if (permissionGranted) {
const imagePickerSuccessResult = await ImagePicker.launchCameraAsync({
const result = await ImagePicker.launchCameraAsync({
mediaTypes: mediaTypeMap[mediaType],
quality: Math.min(Math.max(0, compressImageQuality), 1),
});
const canceled = imagePickerSuccessResult.canceled;
const assets = imagePickerSuccessResult.assets;
if (!result || !result.assets || !result.assets.length || result.canceled) {
return { cancelled: true };
}
// since we only support single photo upload for now we will only be focusing on 0'th element.
const photo = assets && assets[0];

if (canceled === false && photo && photo.height && photo.width && photo.uri) {
let size: Size = {};
if (Platform.OS === 'android') {
const getSize = (): Promise<Size> =>
new Promise((resolve) => {
Image.getSize(photo.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: photo.height,
width: photo.width,
};
}

const photo = result.assets[0];
if (!photo) {
return { cancelled: true };
}
if (photo.mimeType.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
...photo,
cancelled: false,
duration: photo.duration, // in milliseconds
name: 'video_recording_' + date + photo.uri.split('.').pop(),
size: photo.fileSize,
source: 'camera',
type: photo.mimeType,
uri: photo.uri,
...size,
};
} else {
if (photo && photo.height && photo.width && photo.uri) {
let size: Size = {};
if (Platform.OS === 'android') {
const getSize = (): Promise<Size> =>
new Promise((resolve) => {
Image.getSize(photo.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: photo.height,
width: photo.width,
};
}

return {
cancelled: false,
size: photo.fileSize,
source: 'camera',
type: photo.mimeType,
uri: photo.uri,
...size,
};
}
}
}
} catch (error) {
Expand Down
87 changes: 56 additions & 31 deletions package/native-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ try {
ImagePicker = require('react-native-image-picker');
} catch (e) {
console.log(
'The package react-native-image-picker is not installed. Please install the same so as to take photo through camera and upload it.',
'The package react-native-image-picker is not installed. Installing this package will enable capturing photos and videos(for iOS) through the app, and thereby send it.',
);
}

export const takePhoto = ImagePicker
? async ({ compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1 }) => {
? async ({
compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1,
mediaType = Platform.OS === 'ios' ? 'mixed' : 'image',
}) => {
if (Platform.OS === 'android') {
const cameraPermissions = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.CAMERA,
Expand All @@ -29,46 +32,68 @@ export const takePhoto = ImagePicker
}
try {
const result = await ImagePicker.launchCamera({
mediaType,
quality: Math.min(Math.max(0, compressImageQuality), 1),
});
if (!result.assets.length) {
if (!result || !result.assets || !result.assets.length || result.didCancel) {
return {
cancelled: true,
};
}
const photo = result.assets[0];
if (photo.height && photo.width && photo.uri) {
let size: { height?: number; width?: number } = {};
if (Platform.OS === 'android') {
// Height and width returned by ImagePicker are incorrect on Android.
const getSize = (): Promise<{ height: number; width: number }> =>
new Promise((resolve) => {
Image.getSize(photo.uri, (width, height) => {
resolve({ height, width });
const asset = result.assets[0];
if (!asset) {
return {
cancelled: true,
};
}
if (asset.type.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
...asset,
cancelled: false,
duration: asset.duration * 1000,
name: 'video_recording_' + date + asset.fileName.split('.').pop(),
size: asset.fileSize,
source: 'camera',
type: asset.type,
uri: asset.uri,
};
} else {
if (asset.height && asset.width && asset.uri) {
let size: { height?: number; width?: number } = {};
if (Platform.OS === 'android') {
// Height and width returned by ImagePicker are incorrect on Android.
const getSize = (): Promise<{ height: number; width: number }> =>
new Promise((resolve) => {
Image.getSize(asset.uri, (width, height) => {
resolve({ height, width });
});
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
// do nothing
console.warn('Error get image size of picture caputred from camera ', e);
try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
// do nothing
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: asset.height,
width: asset.width,
};
}
} else {
size = {
height: photo.height,
width: photo.width,
return {
cancelled: false,
size: asset.size,
source: 'camera',
type: asset.type,
uri: asset.uri,
...size,
};
}
return {
cancelled: false,
size: photo.size,
source: 'camera',
uri: photo.uri,
...size,
};
}
} catch (e: unknown) {
if (e instanceof Error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { AIState, Channel, Event } from 'stream-chat';

import { useChatContext } from '../../../contexts';
import type { DefaultStreamChatGenerics } from '../../../types/types';
import { useIsOnline } from '../../Chat/hooks/useIsOnline';

export const AIStates = {
Error: 'AI_STATE_ERROR',
Expand All @@ -24,8 +23,7 @@ export const useAIState = <
>(
channel?: Channel<StreamChatGenerics>,
): { aiState: AIState } => {
const { client } = useChatContext<StreamChatGenerics>();
const { isOnline } = useIsOnline<StreamChatGenerics>(client);
const { isOnline } = useChatContext<StreamChatGenerics>();

const [aiState, setAiState] = useState<AIState>(AIStates.Idle);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const AttachmentVideo = (props: AttachmentVideoProps) => {
</View>
)}
<View style={styles.videoView}>
<Recorder height={20} pathFill={white} width={25} />
<Recorder height={20} pathFill={white} width={20} />
{videoDuration ? (
<Text style={[{ color: white }, styles.durationText, durationText]}>
{durationLabel}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';

import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext';
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
Expand Down Expand Up @@ -29,6 +29,7 @@ export const AttachmentPickerSelectionBar = () => {
ImageSelectorIcon,
selectedPicker,
setSelectedPicker,
VideoRecorderSelectorIcon,
} = useAttachmentPickerContext();

const {
Expand Down Expand Up @@ -105,7 +106,9 @@ export const AttachmentPickerSelectionBar = () => {
{hasCameraPicker ? (
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
onPress={takeAndUploadImage}
onPress={() => {
takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed');
}}
testID='take-photo-touchable'
>
<View style={[styles.icon, icon]}>
Expand All @@ -116,6 +119,22 @@ export const AttachmentPickerSelectionBar = () => {
</View>
</TouchableOpacity>
) : null}
{hasCameraPicker && Platform.OS === 'android' ? (
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
onPress={() => {
takeAndUploadImage('video');
}}
testID='take-photo-touchable'
>
<View style={[styles.icon, { marginTop: 4 }, icon]}>
<VideoRecorderSelectorIcon
numberOfImageUploads={imageUploads.length}
selectedPicker={selectedPicker}
/>
</View>
</TouchableOpacity>
) : null}
{!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
Expand Down
Loading

0 comments on commit 1113ce1

Please sign in to comment.