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)} onMembersPress={() => setShowMembers(show => !show)}
/> />
<div id="content" ref={refMessagesContainer}> <div
<Messages /> id="content"
</div> ref={refMessagesContainer}
style={{ position: 'relative' }}
<div id="composer"> >
<Composer isBlurred={shouldBlurBottom} reply={false} /> <div id="messages">
<Messages />
</div>
<div id="composer">
<Composer isBlurred={shouldBlurBottom} reply={false} />
</div>
</div> </div>
</main> </main>
<AnimatePresence enterVariant="fromRight" exitVariant="fromLeft"> <AnimatePresence enterVariant="fromRight" exitVariant="fromLeft">

View File

@ -41,16 +41,20 @@ body,
#content { #content {
overflow: auto; overflow: auto;
padding: 72px 8px 132px 8px; padding: 40px 0px 0px 0px;
height: 100vh; height: 100vh;
margin-top: -56px; margin-top: -56px;
} }
#messages {
padding: 32px 8px;
}
#composer { #composer {
position: absolute; position: sticky;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; z-index: 100;
} }
#members { #members {

View File

@ -1,2 +1,3 @@
export { useBlur } from './use-blur' export { useBlur } from './use-blur'
export { useImageUpload } from './use-image-uploader'
export { useThrottle } from './use-throttle' 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 { useState } from 'react'
import { useImageUpload } from '@status-im/components/hooks'
import { import {
ArrowUpIcon,
AudioIcon, AudioIcon,
ClearIcon,
FormatIcon, FormatIcon,
ImageIcon, ImageIcon,
ReactionIcon, ReactionIcon,
} from '@status-im/icons/20' } from '@status-im/icons/20'
import { BlurView } from 'expo-blur' 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 { IconButton } from '../icon-button'
import { Image } from '../image'
import { Input } from '../input' import { Input } from '../input'
import { Reply } from '../reply' import { Reply } from '../reply'
@ -22,26 +27,36 @@ const Composer = (props: Props) => {
const { isBlurred, reply } = props const { isBlurred, reply } = props
const [isFocused, setIsFocused] = useState(false) 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 ( return (
<BlurView <BlurView
intensity={40} intensity={40}
style={{ style={{
zIndex: 100,
borderRadius: 20, borderRadius: 20,
width: '100%',
}} }}
> >
<YStack <YStack
animation="fast" animation="fast"
backgroundColor={isFocused ? '$background' : '$blurBackground'} backgroundColor={iconButtonBlurred ? '$blurBackground' : '$background'}
shadowColor={!isBlurred || isFocused ? 'rgba(9, 16, 28, 0.08)' : 'none'} shadowColor={iconButtonBlurred ? 'none' : 'rgba(9, 16, 28, 0.08)'}
shadowOffset={{ width: 4, height: !isBlurred || isFocused ? 4 : 0 }} shadowOffset={{ width: 4, height: iconButtonBlurred ? 0 : 4 }}
shadowRadius={20} shadowRadius={20}
borderTopLeftRadius={20} borderTopLeftRadius={20}
borderTopRightRadius={20} borderTopRightRadius={20}
px={16} px={16}
width="100%"
py={12} py={12}
style={{ style={{
elevation: 10, elevation: 10,
@ -68,8 +83,78 @@ const Composer = (props: Props) => {
blurred={isBlurred} blurred={isBlurred}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
onFocus={() => setIsFocused(true)} 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 <XStack
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
@ -77,11 +162,14 @@ const Composer = (props: Props) => {
backgroundColor="transparent" backgroundColor="transparent"
> >
<Stack space={12} flexDirection="row" backgroundColor="transparent"> <Stack space={12} flexDirection="row" backgroundColor="transparent">
<IconButton <label htmlFor="image-uploader-input">
variant="outline" <IconButton
icon={<ImageIcon />} variant="outline"
blurred={iconButtonBlurred} icon={<ImageIcon />}
/> disabled={isImageUploadDisabled}
blurred={iconButtonBlurred}
/>
</label>
<IconButton <IconButton
variant="outline" variant="outline"
icon={<ReactionIcon />} icon={<ReactionIcon />}
@ -94,11 +182,25 @@ const Composer = (props: Props) => {
blurred={iconButtonBlurred} blurred={iconButtonBlurred}
/> />
</Stack> </Stack>
<IconButton {text || imagesData.length > 0 ? (
variant="outline" // TODO fix styles for circular button. Also the color is different from the design and we have layout shift because of the size.
icon={<AudioIcon />} <Button
blurred={iconButtonBlurred} icon={<ArrowUpIcon />}
/> height={32}
size={32}
width={32}
borderRadius={32}
justifyContent="center"
alignItems="center"
type="positive"
/>
) : (
<IconButton
variant="outline"
icon={<AudioIcon />}
blurred={iconButtonBlurred}
/>
)}
</XStack> </XStack>
</YStack> </YStack>
</BlurView> </BlurView>