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:
parent
94a02ba6d6
commit
c0dc3b4497
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { useBlur } from './use-blur'
|
||||
export { useImageUpload } from './use-image-uploader'
|
||||
export { useThrottle } from './use-throttle'
|
||||
|
|
|
@ -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 }
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue