From c0dc3b4497ebd429f68aebc1babe748afd4eb110 Mon Sep 17 00:00:00 2001 From: marcelines Date: Tue, 21 Feb 2023 11:47:00 +0000 Subject: [PATCH] Add image picker to composer (#344) * feat: add image uploader feature * fix: minor fixes * feat: add button when has images or input has value * fix: composer props * fix: minor issues --- apps/vite/src/app.tsx | 17 ++- apps/vite/styles/app.css | 10 +- packages/components/hooks/index.ts | 1 + .../components/hooks/use-image-uploader.ts | 73 ++++++++++ packages/components/src/composer/composer.tsx | 136 +++++++++++++++--- 5 files changed, 211 insertions(+), 26 deletions(-) create mode 100644 packages/components/hooks/use-image-uploader.ts diff --git a/apps/vite/src/app.tsx b/apps/vite/src/app.tsx index 44b332bb..9ce7e2c5 100644 --- a/apps/vite/src/app.tsx +++ b/apps/vite/src/app.tsx @@ -84,12 +84,17 @@ function App() { onMembersPress={() => setShowMembers(show => !show)} /> -
- -
- -
- +
+
+ +
+
+ +
diff --git a/apps/vite/styles/app.css b/apps/vite/styles/app.css index 3bcc1fd1..944fc433 100644 --- a/apps/vite/styles/app.css +++ b/apps/vite/styles/app.css @@ -41,16 +41,20 @@ body, #content { overflow: auto; - padding: 72px 8px 132px 8px; + padding: 40px 0px 0px 0px; height: 100vh; margin-top: -56px; } +#messages { + padding: 32px 8px; +} + #composer { - position: absolute; + position: sticky; bottom: 0; left: 0; - right: 0; + z-index: 100; } #members { diff --git a/packages/components/hooks/index.ts b/packages/components/hooks/index.ts index c5497c4e..0b801443 100644 --- a/packages/components/hooks/index.ts +++ b/packages/components/hooks/index.ts @@ -1,2 +1,3 @@ export { useBlur } from './use-blur' +export { useImageUpload } from './use-image-uploader' export { useThrottle } from './use-throttle' diff --git a/packages/components/hooks/use-image-uploader.ts b/packages/components/hooks/use-image-uploader.ts new file mode 100644 index 00000000..14070901 --- /dev/null +++ b/packages/components/hooks/use-image-uploader.ts @@ -0,0 +1,73 @@ +import { useRef, useState } from 'react' + +interface UseImageUploadReturn { + imagesData: string[] + handleImageUpload: (event: React.ChangeEvent) => void + handleImageRemove: (index: number) => void + imageUploaderInputRef: React.RefObject + isDisabled: boolean +} +const ALLOWED_EXTENSIONS = /(\.jpg|\.jpeg|\.png)$/i +const IMAGES_LIMIT = 6 + +const useImageUpload = (): UseImageUploadReturn => { + const [imagesData, setImagesData] = useState([]) + const imageUploaderInputRef = useRef(null) + + const handleImageUpload = async ( + event: React.ChangeEvent + ) => { + const files = event.target.files + + if (!files) { + return + } + + const filteredFiles = [...files].filter(file => + ALLOWED_EXTENSIONS.test(file.name) + ) + + // Show alert if some files have unsupported formats + if (files.length > filteredFiles.length) { + return alert( + `Some files have unsupported formats. Only .jpg, .jpeg and .png formats are supported.` + ) + } + + if (files.length > IMAGES_LIMIT || imagesData.length > IMAGES_LIMIT) { + return alert( + `You can upload only ${IMAGES_LIMIT} images. Please remove some files and try again.` + ) + } + + const newImagesData: string[] = await Promise.all( + filteredFiles.map(async file => { + const reader = new FileReader() + reader.readAsDataURL(file) + return new Promise(resolve => { + reader.onloadend = () => { + resolve(reader.result as string) + } + }) + }) + ) + + setImagesData(prevState => [...prevState, ...newImagesData]) + } + + const handleImageRemove = (index: number) => { + // Reset input value to trigger onChange event + imageUploaderInputRef.current!.value = '' + setImagesData(prevState => prevState.filter((_, i) => i !== index)) + } + + return { + imagesData, + handleImageUpload, + handleImageRemove, + imageUploaderInputRef, + isDisabled: imagesData.length >= IMAGES_LIMIT, + } +} + +export { useImageUpload } diff --git a/packages/components/src/composer/composer.tsx b/packages/components/src/composer/composer.tsx index 5cad4911..0b9b7f73 100644 --- a/packages/components/src/composer/composer.tsx +++ b/packages/components/src/composer/composer.tsx @@ -1,15 +1,20 @@ import { useState } from 'react' +import { useImageUpload } from '@status-im/components/hooks' import { + ArrowUpIcon, AudioIcon, + ClearIcon, FormatIcon, ImageIcon, ReactionIcon, } from '@status-im/icons/20' import { BlurView } from 'expo-blur' -import { Stack, XStack, YStack } from 'tamagui' +import { AnimatePresence, Stack, XStack, YStack } from 'tamagui' +import { Button } from '../button' import { IconButton } from '../icon-button' +import { Image } from '../image' import { Input } from '../input' import { Reply } from '../reply' @@ -22,26 +27,36 @@ const Composer = (props: Props) => { const { isBlurred, reply } = props const [isFocused, setIsFocused] = useState(false) + const [text, setText] = useState('') - const iconButtonBlurred = isBlurred && !isFocused + const { + imagesData, + handleImageUpload, + handleImageRemove, + imageUploaderInputRef, + isDisabled: isImageUploadDisabled, + } = useImageUpload() + + const iconButtonBlurred = isBlurred && !isFocused && imagesData.length === 0 return ( { blurred={isBlurred} onBlur={() => setIsFocused(false)} onFocus={() => setIsFocused(true)} + onChangeText={setText} /> - + + + {imagesData.length > 0 && ( + + {imagesData.map((imageData, index) => ( + + + handleImageRemove(index)} + cursor="pointer" + justifyContent="center" + alignItems="center" + > + + + + + + + ))} + + )} + { backgroundColor="transparent" > - } - blurred={iconButtonBlurred} - /> + } @@ -94,11 +182,25 @@ const Composer = (props: Props) => { blurred={iconButtonBlurred} /> - } - blurred={iconButtonBlurred} - /> + {text || imagesData.length > 0 ? ( + // TODO fix styles for circular button. Also the color is different from the design and we have layout shift because of the size. +