Skip to content

Commit

Permalink
EditImagesUI: use menu instead of action sheet to add images
Browse files Browse the repository at this point in the history
  • Loading branch information
zetavg committed Jan 21, 2024
1 parent e30d4ae commit fe3de5b
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 115 deletions.
14 changes: 12 additions & 2 deletions App/app/components/Menu/MenuView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Props = {
/** Actions in the menu. */
actions: ReadonlyArray<MenuAction>;
children?: React.ReactNode | undefined;
disabled?: boolean;
};

const NATIVE_STATE_SUPPORTED = Platform.OS === 'ios';
Expand Down Expand Up @@ -60,7 +61,12 @@ function processActions(
});
}

export default function MenuView({ title, actions, ...restProps }: Props) {
export default function MenuView({
title,
actions,
disabled,
...restProps
}: Props) {
const logger = useLogger('MenuView');
const processedActions = useMemo(() => processActions(actions), [actions]);
const handlePressAction = useCallback<
Expand Down Expand Up @@ -99,6 +105,8 @@ export default function MenuView({ title, actions, ...restProps }: Props) {

const { showActionSheet } = useActionSheet();

if (disabled) return restProps.children || null;

if (Platform.OS === 'android') {
// @react-native-menu/menu will not work on Android randomly. Fall back to ActionSheet.
// See: https://github.com/react-native-menu/menu/issues/539
Expand Down Expand Up @@ -131,7 +139,9 @@ export default function MenuView({ title, actions, ...restProps }: Props) {
};
return (
<TouchableWithoutFeedback
onPress={() => handleOpenActionSheetForMenuActions(actions)}
onPress={() =>
!disabled && handleOpenActionSheetForMenuActions(actions)
}
{...restProps}
/>
);
Expand Down
19 changes: 18 additions & 1 deletion App/app/components/Menu/Sample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export default function SampleComponent() {
{
title: 'Destructive Action',
destructive: true,
onPress: () => Alert.alert('"Destructive Action" pressed'),
onPress: () =>
Alert.alert('"Destructive Action" pressed'),
},
],
},
Expand All @@ -161,6 +162,22 @@ export default function SampleComponent() {
<Button title="Show Menu" />
</MenuView>
</StorybookSection>
<StorybookSection title="MenuView disabled" style={{ gap: 8 }}>
<MenuView
disabled
actions={useMemo(
() => [
{
title: 'Action',
onPress: () => Alert.alert('"Action" pressed'),
},
],
[],
)}
>
<Button title="Show Menu" disabled />
</MenuView>
</StorybookSection>
</StorybookStoryContainer>
);
}
166 changes: 90 additions & 76 deletions App/app/data/images/useImageSelector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { Alert, Platform } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
Expand All @@ -7,6 +7,8 @@ import humanFileSize from '@app/utils/humanFileSize';

import useActionSheet from '@app/hooks/useActionSheet';

import { MenuAction } from '@app/components/Menu';

import processAssets, { ImageAsset, ImageD } from './processAssets';

type Options = {
Expand Down Expand Up @@ -134,89 +136,101 @@ export default function useImageSelector() {
[],
);

const getSelectImageActions = useCallback<
(
callback: (images: ReadonlyArray<ImageData> | null) => void,
options: Options,
) => ReadonlyArray<MenuAction>
>(
(callback, options = {}) => [
{
title: 'Take Picture from Camera',
sfSymbolName: 'camera',
onPress: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const assets = await takePictureFromCamera(options);
if (!assets) {
callback(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(assets);
callback(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
callback(null);
}
},
},
{
title: 'Select from Photo Library',
sfSymbolName: 'photo.on.rectangle',
onPress: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const assets = await selectImageFromLibrary(options);
if (!assets) {
callback(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(assets);
callback(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
callback(null);
}
},
},
{
title: 'Select from Files',
sfSymbolName: 'folder',
onPress: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const imageAssets = await selectImageFromFile(options);
if (!imageAssets || imageAssets.length <= 0) {
callback(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(imageAssets);
callback(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
callback(null);
}
},
},
],
[selectImageFromFile, selectImageFromLibrary, takePictureFromCamera],
);

const selectImage = useCallback(
(options: Options = {}): Promise<ReadonlyArray<ImageData> | null> => {
return new Promise(resolve => {
const actions = getSelectImageActions(resolve, options);
showActionSheet(
[
{
name: 'Take Picture from Camera',
onSelect: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const assets = await takePictureFromCamera(options);
if (!assets) {
resolve(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(assets);
resolve(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
resolve(null);
}
},
},
{
name: 'Select from Photo Library',
onSelect: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const assets = await selectImageFromLibrary(options);
if (!assets) {
resolve(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(assets);
resolve(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
resolve(null);
}
},
},
{
name: 'Select from Files',
onSelect: async () => {
try {
if (options.onUserSelectStart) options.onUserSelectStart();
const imageAssets = await selectImageFromFile(options);
if (!imageAssets || imageAssets.length <= 0) {
resolve(null);
return;
}
if (options.onUserSelected) options.onUserSelected();
const images = await processAssets(imageAssets);
resolve(images);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : 'unknown error',
);
resolve(null);
}
},
},
],
actions.map(a => ({
name: a.title || '',
onSelect: a.onPress || (() => {}),
})),
{ onCancel: () => resolve(null) },
);
});
},
[
selectImageFromFile,
selectImageFromLibrary,
showActionSheet,
takePictureFromCamera,
],
[getSelectImageActions, showActionSheet],
);

return selectImage;
return { selectImage, getSelectImageActions };
}
81 changes: 45 additions & 36 deletions App/app/features/inventory/components/EditImagesUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import commonStyles from '@app/utils/commonStyles';
import useColors from '@app/hooks/useColors';
import useLogger from '@app/hooks/useLogger';

import { MenuView } from '@app/components/Menu';
import UIGroup from '@app/components/UIGroup';

import { imageLoadingPlaceholder } from '@app/images';
Expand Down Expand Up @@ -182,34 +183,33 @@ export function EditImagesUI({
}, [db, loadedImageMap, loadedItemImageData, logger]);

const [isAddingImage, setIsAddingImage] = useState(false);
const selectImage = useImageSelector();
const handleAddImage = useCallback(async () => {
try {
const images = await selectImage({
onUserSelectStart: () => setIsAddingImage(true),
selectionLimit: IMAGES_LIMIT - (itemImageData?.length || 0),
});
if (!images) return;

hasChangesRef.current = true;
LayoutAnimation.configureNext(DEFAULT_LAYOUT_ANIMATION_CONFIG);
setItemImageData(origData => [
...(origData || []),
...images.map(i => ({
__type: 'unsaved_image_data' as const,
__id: uuid(),
...i,
})),
]);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : e?.toString(),
);
} finally {
setIsAddingImage(false);
}
}, [hasChangesRef, itemImageData?.length, selectImage]);
const { selectImage, getSelectImageActions } = useImageSelector();
const handleAddImages = useCallback(
async (images: ReadonlyArray<ImageData> | null) => {
try {
if (!images) return;

hasChangesRef.current = true;
LayoutAnimation.configureNext(DEFAULT_LAYOUT_ANIMATION_CONFIG);
setItemImageData(origData => [
...(origData || []),
...images.map(i => ({
__type: 'unsaved_image_data' as const,
__id: uuid(),
...i,
})),
]);
} catch (e) {
Alert.alert(
'An Error Occurred',
e instanceof Error ? e.message : e?.toString(),
);
} finally {
setIsAddingImage(false);
}
},
[hasChangesRef],
);

const imageUiWorking = loadingDelayed || itemImageLoading || isAddingImage;
const imageUiWorkingRef = useRef(imageUiWorking);
Expand Down Expand Up @@ -318,6 +318,9 @@ export function EditImagesUI({
selectors.settings.uiShowDetailedInstructions,
);

const addImageDisabled =
imageUiWorking || (itemImageData || []).length >= IMAGES_LIMIT;

return (
<>
<UIGroup
Expand Down Expand Up @@ -415,14 +418,20 @@ export function EditImagesUI({
)}
</>
)}
<UIGroup.ListItem
label="Add Image..."
button
onPress={handleAddImage}
disabled={
imageUiWorking || (itemImageData || []).length >= IMAGES_LIMIT
}
/>
<MenuView
actions={getSelectImageActions(handleAddImages, {
onUserSelectStart: () => setIsAddingImage(true),
selectionLimit: IMAGES_LIMIT - (itemImageData?.length || 0),
})}
disabled={addImageDisabled}
>
<UIGroup.ListItem
label="Add Image..."
button
onPress={() => {}}
disabled={addImageDisabled}
/>
</MenuView>
</UIGroup>
<ImageView
images={
Expand Down

0 comments on commit fe3de5b

Please sign in to comment.