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
Expand Up @@ -3,21 +3,23 @@ import * as Sentry from '@sentry/react-native';

import * as ImagePicker from 'expo-image-picker';
import React, {useCallback, useEffect, useState} from 'react';
import {useController} from 'react-hook-form';
import {ColorValue, LayoutChangeEvent, Modal, StyleSheet, TouchableHighlight, View} from 'react-native';
import {FieldPathByValue, FieldValues, useController} from 'react-hook-form';
import {ColorValue, LayoutChangeEvent, Modal, StyleSheet, TouchableHighlight} from 'react-native';

import {Button} from 'components/content/Button';
import {NetworkImage} from 'components/content/carousel/NetworkImage';
import {HStack, VStack, ViewProps} from 'components/core';
import {ImageAndCaption, ObservationFormData} from 'components/observations/ObservationFormData';
import {ObservationImageEditView} from 'components/observations/ObservationImageEditView';
import {HStack, VStack, View, ViewProps} from 'components/core';
import {ImageCaptionFieldEditView} from 'components/form/ImageCaptionFieldEditView';
import {ImageAndCaption, ImagePickerAssetWithCaption} from 'components/observations/ObservationFormData';
import {getUploader} from 'components/observations/uploader/ObservationsUploader';
import {Body, BodyBlack, BodySm} from 'components/text';
import {LoggerContext, LoggerProps} from 'loggerContext';
import Toast from 'react-native-toast-message';
import {colorLookup} from 'theme';

export const ImageCaptionField: React.FC<{
type ImageAssetArray = ImagePickerAssetWithCaption[] | undefined | null;

const EditImageCaptionField: React.FC<{
image: ImageAndCaption | null;
onUpdateImage: (image: ImageAndCaption) => void;
onDismiss: () => void;
Expand Down Expand Up @@ -56,17 +58,22 @@ export const ImageCaptionField: React.FC<{

return (
<Modal visible={visible} animationType="none" transparent presentationStyle="overFullScreen" onRequestClose={onDismiss}>
<ObservationImageEditView onDismiss={handleDismiss} onViewDismissed={handleDismiss} onSetCaption={onSetCaption} initialCaption={image?.caption} />
<ImageCaptionFieldEditView onDismiss={handleDismiss} onViewDismissed={handleDismiss} onSetCaption={onSetCaption} initialCaption={image?.caption} />
</Modal>
);
};

export const useObservationPickImages = ({maxImageCount, disable}: {maxImageCount: number; disable: boolean}) => {
interface ImagePickerProps {
images: ImageAssetArray;
maxImageCount: number;
disable: boolean;
onSaveImages: (newImages: ImageAssetArray) => void;
}

const useImagePicker = ({images, maxImageCount, disable, onSaveImages}: ImagePickerProps) => {
const [imagePermissions] = ImagePicker.useMediaLibraryPermissions();
const missingImagePermissions = imagePermissions !== null && !imagePermissions.granted && !imagePermissions.canAskAgain;

const {field} = useController<ObservationFormData, 'images'>({name: 'images', defaultValue: []});
const images = field.value;
const imageCount = images?.length ?? 0;

const isDisabled = imageCount === maxImageCount || disable || missingImagePermissions;
Expand All @@ -82,12 +89,13 @@ export const useObservationPickImages = ({maxImageCount, disable}: {maxImageCoun
mediaTypes: ['images', 'videos', 'livePhotos'],
preferredAssetRepresentationMode: ImagePicker.UIImagePickerPreferredAssetRepresentationMode.Compatible,
quality: 0.9,
selectionLimit: maxImageCount - (images?.length ?? 0),
selectionLimit: maxImageCount - imageCount,
});

if (!result.canceled) {
const newImages = (images ?? []).concat(result.assets.map(image => ({image}))).slice(0, maxImageCount);
field.onChange(newImages);
const newImages = result.assets.map(image => ({image}));
const updatedImages = (images ?? []).concat(newImages).slice(0, maxImageCount);
onSaveImages(updatedImages);
}
} catch (error) {
logger.error('ImagePicker error', {error});
Expand All @@ -104,18 +112,28 @@ export const useObservationPickImages = ({maxImageCount, disable}: {maxImageCoun
});
}
})();
}, [images, logger, field, maxImageCount]);
}, [images, logger, imageCount, maxImageCount, onSaveImages]);

return {onPickImage: pickImage, isDisabled};
};

interface ObservationAddImageButtonProps extends ViewProps {
interface AddImageFromPickerButtonProps<TFieldValues extends FieldValues, TKey extends FieldPathByValue<TFieldValues, ImageAssetArray>> extends ViewProps {
name: TKey;
maxImageCount: number;
disable?: boolean;
space?: number;
}

export const ObservationAddImageButton: React.FC<ObservationAddImageButtonProps> = ({maxImageCount, disable = false, space = 4, ...props}) => {
const _AddImageFromPickerButton = <TFieldValues extends FieldValues, TKey extends FieldPathByValue<TFieldValues, ImageAssetArray>>({
name,
maxImageCount,
disable = false,
space = 4,
...props
}: AddImageFromPickerButtonProps<TFieldValues, TKey>) => {
const {field} = useController<TFieldValues, TKey>({name: name});
const images = field.value;

const renderAddImageButton = useCallback(
({textColor}: {textColor: ColorValue}) => (
<HStack alignItems="center" space={space}>
Expand All @@ -126,17 +144,41 @@ export const ObservationAddImageButton: React.FC<ObservationAddImageButtonProps>
[space],
);

const {onPickImage, isDisabled} = useObservationPickImages({maxImageCount, disable});
const onSaveImages = useCallback(
(updatedImages: ImageAssetArray) => {
field.onChange(updatedImages);
},
[field],
);

const {onPickImage, isDisabled} = useImagePicker({images, maxImageCount, disable, onSaveImages});

return <Button buttonStyle="normal" onPress={onPickImage} disabled={isDisabled} renderChildren={renderAddImageButton} {...props} />;
};

export const ObservationImagePicker: React.FC<{
export type AddImageFromPickerButtonComponent<TFieldValues extends FieldValues> = <TFieldName extends FieldPathByValue<TFieldValues, ImageAssetArray>>(
props: React.PropsWithoutRef<AddImageFromPickerButtonProps<TFieldValues, TFieldName>>,
) => JSX.Element;

export const AddImageFromPickerButton = _AddImageFromPickerButton as (<TFieldValues extends FieldValues, TFieldName extends FieldPathByValue<TFieldValues, ImageAssetArray>>(
props: React.PropsWithoutRef<AddImageFromPickerButtonProps<TFieldValues, TFieldName>>,
) => JSX.Element) & {displayName?: string};

AddImageFromPickerButton.displayName = 'AddImagePickerButton';

interface ImageCaptionFieldProps<TFieldValues extends FieldValues, TKey extends FieldPathByValue<TFieldValues, ImageAssetArray>> {
name: TKey;
maxImageCount: number;
onModalDisplayed: (isOpen: boolean) => void;
}> = ({maxImageCount, onModalDisplayed}) => {
const {field} = useController<ObservationFormData, 'images'>({name: 'images', defaultValue: []});
const images = field.value;
}

const _ImageCaptionField = <TFieldValues extends FieldValues, TKey extends FieldPathByValue<TFieldValues, ImageAssetArray>>({
name,
maxImageCount,
onModalDisplayed,
}: ImageCaptionFieldProps<TFieldValues, TKey>) => {
const {field} = useController<TFieldValues, TKey>({name: name});
const images = field.value as ImageAssetArray;

const [editingImage, setEditingImage] = useState<ImageAndCaption | null>(null);

Expand Down Expand Up @@ -235,16 +277,26 @@ export const ObservationImagePicker: React.FC<{
</VStack>
)}
{images?.length === 0 && <Body>You can add up to {maxImageCount} images.</Body>}
<ImageCaptionField image={editingImage} onUpdateImage={onUpdateImageCaption} onDismiss={onDismiss} onModalDisplayed={onModalDisplayed} />
<EditImageCaptionField image={editingImage} onUpdateImage={onUpdateImageCaption} onDismiss={onDismiss} onModalDisplayed={onModalDisplayed} />
</>
);
};

export type ImageCaptionFieldComponent<TFieldValues extends FieldValues> = <TFieldName extends FieldPathByValue<TFieldValues, ImageAssetArray>>(
props: React.PropsWithoutRef<ImageCaptionFieldProps<TFieldValues, TFieldName>>,
) => JSX.Element;

export const ImageCaptionField = _ImageCaptionField as (<TFieldValues extends FieldValues, TFieldName extends FieldPathByValue<TFieldValues, ImageAssetArray>>(
props: React.PropsWithoutRef<ImageCaptionFieldProps<TFieldValues, TFieldName>>,
) => JSX.Element) & {displayName?: string};

ImageCaptionField.displayName = 'ImageCaptionField';

type SizingProps = Omit<ViewProps, 'onLayout' | 'children'> & {
children: (size: {width: number; height: number}) => React.ReactNode;
};

export const ImageSizingView: React.FC<SizingProps> = ({children, ...props}) => {
const ImageSizingView: React.FC<SizingProps> = ({children, ...props}) => {
const [state, setState] = useState<{width: number; height: number} | null>(null);

const handleLayout = useCallback((event: LayoutChangeEvent) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const AUTO_DISMISS_DRAGGING_COMPLETE_HEIGHT = 320;
const AUTO_DISMISS_DRAGGING_HEIGHT = 200;
const AUTO_DISMISS_VELOCITY = 3;

export const ObservationImageEditView: React.FC<Props> = ({onSetCaption, onDismiss, initialCaption, autoDismiss = true}) => {
export const ImageCaptionFieldEditView: React.FC<Props> = ({onSetCaption, onDismiss, initialCaption, autoDismiss = true}) => {
const ref = useRef<View>(null);
const fadeAnim = useRef(new Animated.Value(0)); // Initial value for opacity: 0

Expand Down
Loading