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
This commit is contained in:
marcelines 2023-02-21 11:47:00 +00:00 committed by GitHub
parent 94a02ba6d6
commit c0dc3b4497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 26 deletions

View File

@ -84,12 +84,17 @@ function App() {
onMembersPress={() => setShowMembers(show => !show)}
/>
<div id="content" ref={refMessagesContainer}>
<Messages />
</div>
<div id="composer">
<Composer isBlurred={shouldBlurBottom} reply={false} />
<div
id="content"
ref={refMessagesContainer}
style={{ position: 'relative' }}
>
<div id="messages">
<Messages />
</div>
<div id="composer">
<Composer isBlurred={shouldBlurBottom} reply={false} />
</div>
</div>
</main>
<AnimatePresence enterVariant="fromRight" exitVariant="fromLeft">

View File

@ -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 {

View File

@ -1,2 +1,3 @@
export { useBlur } from './use-blur'
export { useImageUpload } from './use-image-uploader'
export { useThrottle } from './use-throttle'

View File

@ -0,0 +1,73 @@
import { useRef, useState } from 'react'
interface UseImageUploadReturn {
imagesData: string[]
handleImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void
handleImageRemove: (index: number) => void
imageUploaderInputRef: React.RefObject<HTMLInputElement>
isDisabled: boolean
}
const ALLOWED_EXTENSIONS = /(\.jpg|\.jpeg|\.png)$/i
const IMAGES_LIMIT = 6
const useImageUpload = (): UseImageUploadReturn => {
const [imagesData, setImagesData] = useState<string[]>([])
const imageUploaderInputRef = useRef<HTMLInputElement>(null)
const handleImageUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
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 }

View File

@ -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 (
<BlurView
intensity={40}
style={{
zIndex: 100,
borderRadius: 20,
width: '100%',
}}
>
<YStack
animation="fast"
backgroundColor={isFocused ? '$background' : '$blurBackground'}
shadowColor={!isBlurred || isFocused ? 'rgba(9, 16, 28, 0.08)' : 'none'}
shadowOffset={{ width: 4, height: !isBlurred || isFocused ? 4 : 0 }}
backgroundColor={iconButtonBlurred ? '$blurBackground' : '$background'}
shadowColor={iconButtonBlurred ? 'none' : 'rgba(9, 16, 28, 0.08)'}
shadowOffset={{ width: 4, height: iconButtonBlurred ? 0 : 4 }}
shadowRadius={20}
borderTopLeftRadius={20}
borderTopRightRadius={20}
px={16}
width="100%"
py={12}
style={{
elevation: 10,
@ -68,8 +83,78 @@ const Composer = (props: Props) => {
blurred={isBlurred}
onBlur={() => setIsFocused(false)}
onFocus={() => setIsFocused(true)}
onChangeText={setText}
/>
<input
ref={imageUploaderInputRef}
type="file"
multiple
onChange={handleImageUpload}
hidden
id="image-uploader-input"
style={{
display: 'none',
}}
/>
<AnimatePresence>
{imagesData.length > 0 && (
<XStack
key="images-thumbnails"
paddingTop={12}
paddingBottom={8}
overflow="scroll"
>
{imagesData.map((imageData, index) => (
<Stack
key={index + imageData}
mr={12}
position="relative"
justifyContent="flex-end"
flexDirection="row"
alignItems="flex-start"
animation={[
'fast',
{
opacity: {
overshootClamping: true,
},
},
]}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
opacity={1}
>
<Image
src={imageData}
width={56}
height={56}
radius={12}
aspectRatio={1}
/>
<Stack
zIndex={8}
onPress={() => handleImageRemove(index)}
cursor="pointer"
justifyContent="center"
alignItems="center"
>
<Stack
backgroundColor="$background"
width={15}
height={15}
borderRadius={7}
position="absolute"
zIndex={1}
/>
<Stack position="absolute" zIndex={2} width={20}>
<ClearIcon color="$neutral-50" />
</Stack>
</Stack>
</Stack>
))}
</XStack>
)}
</AnimatePresence>
<XStack
alignItems="center"
justifyContent="space-between"
@ -77,11 +162,14 @@ const Composer = (props: Props) => {
backgroundColor="transparent"
>
<Stack space={12} flexDirection="row" backgroundColor="transparent">
<IconButton
variant="outline"
icon={<ImageIcon />}
blurred={iconButtonBlurred}
/>
<label htmlFor="image-uploader-input">
<IconButton
variant="outline"
icon={<ImageIcon />}
disabled={isImageUploadDisabled}
blurred={iconButtonBlurred}
/>
</label>
<IconButton
variant="outline"
icon={<ReactionIcon />}
@ -94,11 +182,25 @@ const Composer = (props: Props) => {
blurred={iconButtonBlurred}
/>
</Stack>
<IconButton
variant="outline"
icon={<AudioIcon />}
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.
<Button
icon={<ArrowUpIcon />}
height={32}
size={32}
width={32}
borderRadius={32}
justifyContent="center"
alignItems="center"
type="positive"
/>
) : (
<IconButton
variant="outline"
icon={<AudioIcon />}
blurred={iconButtonBlurred}
/>
)}
</XStack>
</YStack>
</BlurView>