From a50012486930f079183f0680a886ed690adf62fc Mon Sep 17 00:00:00 2001 From: Matthew Horan Date: Fri, 15 Mar 2024 11:32:18 -0400 Subject: [PATCH] Indicate media upload progress via spinner Closes #82. --- __tests__/usecase/buffers/ui/UploadButton.tsx | 29 ++++++++--- src/usecase/buffers/ui/UploadButton.tsx | 17 ++++++- src/usecase/buffers/ui/UploadSpinner.tsx | 48 +++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 src/usecase/buffers/ui/UploadSpinner.tsx diff --git a/__tests__/usecase/buffers/ui/UploadButton.tsx b/__tests__/usecase/buffers/ui/UploadButton.tsx index 331b271..bac1e2d 100644 --- a/__tests__/usecase/buffers/ui/UploadButton.tsx +++ b/__tests__/usecase/buffers/ui/UploadButton.tsx @@ -1,6 +1,6 @@ +import { fireEvent, render, screen } from '@testing-library/react-native'; import * as FileSystem from 'expo-file-system'; import * as ImagePicker from 'expo-image-picker'; -import { fireEvent, render, screen } from '@testing-library/react-native'; import UploadButton from '../../../../src/usecase/buffers/ui/UploadButton'; jest.mock('expo-file-system', () => { @@ -13,6 +13,8 @@ jest.mock('expo-file-system', () => { const mockedFileSystem = jest.mocked(FileSystem); describe(UploadButton, () => { + let resolveUpload: (result: FileSystem.FileSystemUploadResult) => void; + beforeEach(() => { jest.clearAllMocks(); jest @@ -24,10 +26,7 @@ describe(UploadButton, () => { }); mockedFileSystem.uploadAsync.mockImplementation(() => { - return Promise.resolve({ - status: 200, - body: 'https://example.com/image.jpg' - } as FileSystem.FileSystemUploadResult); + return new Promise((resolve) => (resolveUpload = resolve)); }); }); @@ -44,7 +43,15 @@ describe(UploadButton, () => { fireEvent(button, 'press'); - await new Promise(process.nextTick); + await screen.findByLabelText('Image Uploading'); + + resolveUpload({ + status: 200, + body: 'https://example.com/image.jpg' + } as FileSystem.FileSystemUploadResult); + + await screen.findByLabelText('Upload Image'); + expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalledWith({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsMultipleSelection: false, @@ -112,7 +119,15 @@ describe(UploadButton, () => { fireEvent(button, 'press'); - await new Promise(process.nextTick); + await screen.findByLabelText('Image Uploading'); + + resolveUpload({ + status: 200, + body: 'https://example.com/image.jpg' + } as FileSystem.FileSystemUploadResult); + + await screen.findByLabelText('Upload Image'); + expect(FileSystem.uploadAsync).toHaveBeenCalledWith( 'https://example.com', 'file:///tmp/image.jpg', diff --git a/src/usecase/buffers/ui/UploadButton.tsx b/src/usecase/buffers/ui/UploadButton.tsx index 5038537..55b8580 100644 --- a/src/usecase/buffers/ui/UploadButton.tsx +++ b/src/usecase/buffers/ui/UploadButton.tsx @@ -2,7 +2,9 @@ import { MaterialIcons } from '@expo/vector-icons'; import { Buffer } from 'buffer'; import * as FileSystem from 'expo-file-system'; import * as ImagePicker from 'expo-image-picker'; -import { StyleProp, TouchableOpacity, ViewStyle } from 'react-native'; +import { StyleProp, TouchableOpacity, View, ViewStyle } from 'react-native'; +import UploadSpinner from './UploadSpinner'; +import { useState } from 'react'; interface Props { onUpload: (url: string) => void; @@ -28,6 +30,8 @@ const UploadButton: React.FC = ({ ...uploadOptions } }) => { + const [showSpinner, setShowSpinner] = useState(false); + const takePhoto = async () => { const permission = await ImagePicker.requestCameraPermissionsAsync(); @@ -58,6 +62,7 @@ const UploadButton: React.FC = ({ if (pickerResult.canceled) { return; } else { + setShowSpinner(true); const uploadUrl = await uploadImage(pickerResult.assets[0].uri); const matches = uploadUrl.match(new RegExp(uploadOptionsRegexp)); if (!matches) return alert('Failed to extract URL from response'); @@ -65,6 +70,8 @@ const UploadButton: React.FC = ({ } } catch (e) { alert('Upload failed'); + } finally { + setShowSpinner(false); } }; @@ -96,6 +103,14 @@ const UploadButton: React.FC = ({ ) return; + if (showSpinner) { + return ( + + + + ); + } + return ( { + const rotation = useSharedValue(0); + + const animatedStyles = useAnimatedStyle(() => { + return { + transform: [ + { + rotateZ: `${rotation.value}deg` + } + ] + }; + }, [rotation.value]); + + useEffect(() => { + rotation.value = withRepeat( + withTiming(360, { + duration: 1000, + easing: Easing.linear + }), + 200 + ); + return () => cancelAnimation(rotation); + }, [rotation]); + + return ( + + ); +}; + +export default UploadSpinner;