Feature/attachment (#2)

* File messages started

* Type fixes and cleanup

* File message design applied, file preview added

* Code cleanup (#3)

Co-authored-by: Alex <alexdemchenko@yahoo.com>
This commit is contained in:
Volodymyr Smolianinov
2020-08-20 21:34:47 +02:00
committed by GitHub
parent 981b1f6bd7
commit d534c8d13c
24 changed files with 359 additions and 42 deletions

View File

@@ -4,6 +4,6 @@
import { AppRegistry } from 'react-native'
import { name as appName } from './app.json'
import App from './src/App'
import AppContainer from './src/AppContainer'
AppRegistry.registerComponent(appName, () => App)
AppRegistry.registerComponent(appName, () => AppContainer)

View File

@@ -236,6 +236,8 @@ PODS:
- React-cxxreact (= 0.63.2)
- React-jsi (= 0.63.2)
- React-jsinspector (0.63.2)
- react-native-document-picker (3.5.4):
- React
- react-native-image-picker (2.3.3):
- React
- react-native-safe-area-context (3.1.4):
@@ -300,6 +302,8 @@ PODS:
- React-Core (= 0.63.2)
- React-cxxreact (= 0.63.2)
- React-jsi (= 0.63.2)
- RNFileViewer (2.1.1):
- React
- Yoga (1.14.0)
- YogaKit (1.18.1):
- Yoga (~> 1.14)
@@ -341,6 +345,7 @@ DEPENDENCIES:
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@@ -353,6 +358,7 @@ DEPENDENCIES:
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -401,6 +407,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-safe-area-context:
@@ -425,6 +433,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/Vibration"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
RNFileViewer:
:path: "../node_modules/react-native-file-viewer"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -455,6 +465,7 @@ SPEC CHECKSUMS:
React-jsi: 54245e1d5f4b690dec614a73a3795964eeef13a8
React-jsiexecutor: 8ca588cc921e70590820ce72b8789b02c67cce38
React-jsinspector: b14e62ebe7a66e9231e9581279909f2fc3db6606
react-native-document-picker: 4921045c5c6c78243010f784eae0a3f4b2af7799
react-native-image-picker: 3d3f85baabca60a00b75fb8facc1376db7bbdafa
react-native-safe-area-context: eb91fe1fb3f7b87d9c30a7f0808407d8569d539d
React-RCTActionSheet: 910163b6b09685a35c4ebbc52b66d1bfbbe39fc5
@@ -467,6 +478,7 @@ SPEC CHECKSUMS:
React-RCTText: 1b6773e776e4b33f90468c20fe3b16ca3e224bb8
React-RCTVibration: 4d2e726957f4087449739b595f107c0d4b6c2d2d
ReactCommon: a0a1edbebcac5e91338371b72ffc66aa822792ce
RNFileViewer: db62d60dd19007c54b2c959b5e675a46d59f8a43
Yoga: 7740b94929bbacbddda59bf115b5317e9a161598
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

View File

@@ -13,8 +13,11 @@
"test": "jest"
},
"dependencies": {
"@expo/react-native-action-sheet": "^3.8.0",
"react": "^16.13.1",
"react-native": "^0.63.2",
"react-native-document-picker": "^3.5.4",
"react-native-file-viewer": "^2.1.1",
"react-native-image-picker": "^2.3.3",
"react-native-safe-area-context": "^3.1.4"
},

View File

@@ -1,10 +1,13 @@
import { useActionSheet } from '@expo/react-native-action-sheet'
import {
Chat,
MessageType,
SendImageCallback,
SendAttachmentCallback,
} from '@flyerhq/react-native-chat-ui'
import React, { useState } from 'react'
import { StatusBar } from 'react-native'
import DocumentPicker from 'react-native-document-picker'
import FileViewer from 'react-native-file-viewer'
import ImagePicker from 'react-native-image-picker'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import data from './messages.json'
@@ -12,15 +15,61 @@ import users from './users.json'
const App = () => {
const [messages, setMessages] = useState(data as MessageType.Any[])
const { showActionSheetWithOptions } = useActionSheet()
const handleAttachmentPress = (send: SendImageCallback) => {
const handleAttachmentPress = (sendAttachment: SendAttachmentCallback) => {
showActionSheetWithOptions(
{
options: ['Photo', 'File', 'Cancel'],
cancelButtonIndex: 2,
},
(buttonIndex) => {
switch (buttonIndex) {
case 0:
handleImageSelection(sendAttachment)
break
case 1:
handleFileSelection(sendAttachment)
break
}
}
)
}
const handleFilePress = async (file: MessageType.File) => {
try {
await FileViewer.open(file.url, { showOpenWithDialog: true })
} catch {}
}
const handleFileSelection = async (
sendAttachment: SendAttachmentCallback
) => {
try {
const response = await DocumentPicker.pick({
type: [DocumentPicker.types.allFiles],
})
sendAttachment({
mimeType: response.type,
name: response.name,
size: response.size,
url: response.uri,
})
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
// Handle error
}
}
}
const handleImageSelection = (sendAttachment: SendAttachmentCallback) => {
ImagePicker.showImagePicker(
{ maxWidth: 1440, quality: 0.7 },
(response) => {
if (response.data) {
send({
sendAttachment({
height: response.height,
imageUrl: 'data:image/jpeg;base64,' + response.data,
url: 'data:image/jpeg;base64,' + response.data,
width: response.width,
})
}
@@ -38,6 +87,7 @@ const App = () => {
<Chat
messages={messages}
onAttachmentPress={handleAttachmentPress}
onFilePress={handleFilePress}
onSendPress={handleSendPress}
user={users[0]}
/>

View File

@@ -0,0 +1,13 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import React from 'react'
import App from './App'
const AppContainer = () => {
return (
<ActionSheetProvider>
<App />
</ActionSheetProvider>
)
}
export default AppContainer

View File

@@ -1213,6 +1213,14 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@expo/react-native-action-sheet@^3.8.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-3.8.0.tgz#0db8b70ea8550ceb2983abda8584efa3a61d7389"
integrity sha512-tCfwysuqy0sfaN+aA98IKUrwCLKsbDHSYLcnHrx9wNbawOHNez8rSeFtieAS48/HyrPI75yg/ZGvxe6UsJRS8Q==
dependencies:
"@types/hoist-non-react-statics" "^3.3.1"
hoist-non-react-statics "^3.3.0"
"@hapi/address@2.x.x":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
@@ -1702,6 +1710,14 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -3904,6 +3920,13 @@ hermes-engine@~0.5.0:
resolved "https://registry.yarnpkg.com/hermes-engine/-/hermes-engine-0.5.1.tgz#601115e4b1e0a17d9aa91243b96277de4e926e09"
integrity sha512-hLwqh8dejHayjlpvZY40e1aDCDvyP98cWx/L5DhAjSJLH8g4z9Tp08D7y4+3vErDsncPOdf1bxm+zUWpx0/Fxg==
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.5"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
@@ -6396,11 +6419,26 @@ react-is@^16.12.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
version "16.11.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa"
integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==
react-native-document-picker@^3.5.4:
version "3.5.4"
resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-3.5.4.tgz#4b9f261ea0d52e91d571670caaf55923ceb80f5f"
integrity sha512-ZKGAa8ztQ7zA1eE95OCiNsI/Q6fiq1Q3es8MyOEakBkWcX9avWNYaJUrbv/8v80Vo4RzcNxsO3wT6a2hgfpz7A==
react-native-file-viewer@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-native-file-viewer/-/react-native-file-viewer-2.1.1.tgz#d0b4c51e9b38870ba2b9fb1d436df844e974f087"
integrity sha512-Q5OtfA+VlprLaXmKk1tH/IFzMHwa2B5YnR4xfoEKOJ53UI5BKvYc67Y/ammUUog7bUwPkUBWenwd4MxBTi1R+g==
react-native-image-picker@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-2.3.3.tgz#eaacd4a1ed9e9887613be31f0765478ca2f18a51"

View File

@@ -1,12 +1,23 @@
import { MessageType, Size, User } from '../src/types'
export const fileMessage: MessageType.File = {
authorId: 'userId',
id: 'uuidv4',
mimeType: 'application/pdf',
name: 'flyer.pdf',
size: 15000,
timestamp: 0,
type: 'file',
url: 'file:///Users/admin/flyer.pdf',
}
export const imageMessage: MessageType.Image = {
authorId: 'userId',
height: 100,
id: 'uuidv4',
imageUrl: 'https://avatars1.githubusercontent.com/u/59206044',
timestamp: 0,
type: 'image',
url: 'https://avatars1.githubusercontent.com/u/59206044',
width: 100,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

View File

@@ -27,7 +27,6 @@ export const AttachmentButton = ({
{...touchableOpacityProps}
onPress={handlePress}
>
{/* type-coverage:ignore-next-line */}
<Image source={require('../../assets/icon-attachment.png')} />
</TouchableOpacity>
)

View File

@@ -25,6 +25,7 @@ export interface ChatProps extends InputProps {
export const Chat = ({
messages,
onAttachmentPress,
onFilePress,
onSendPress,
textInputProps,
user,
@@ -37,7 +38,7 @@ export const Chat = ({
const [stackEntry, setStackEntry] = React.useState<StatusBarProps>({})
const images = messages
.filter((message): message is MessageType.Image => message.type === 'image')
.map((message) => ({ uri: message.imageUrl }))
.map((message) => ({ uri: message.url }))
.reverse()
const list = React.useRef<FlatList<MessageType.Any>>(null)
@@ -65,8 +66,9 @@ export const Chat = ({
return (
<Message
message={item}
onImagePress={(imageUrl) => {
setImageViewIndex(images.findIndex((image) => image.uri === imageUrl))
onFilePress={onFilePress}
onImagePress={(url) => {
setImageViewIndex(images.findIndex((image) => image.uri === url))
setIsImageViewVisible(true)
setStackEntry(
StatusBar.pushStackEntry({

View File

@@ -1,6 +1,11 @@
import { fireEvent, render, waitFor } from '@testing-library/react-native'
import * as React from 'react'
import { imageMessage, textMessage, user } from '../../../../jest/fixtures'
import {
fileMessage,
imageMessage,
textMessage,
user,
} from '../../../../jest/fixtures'
import { Chat } from '../Chat'
describe('chat', () => {
@@ -34,4 +39,23 @@ describe('chat', () => {
fireEvent.press(button)
expect(onSendPress).toHaveBeenCalledWith(textMessage)
})
it('opens file on a file message tap', () => {
expect.assertions(1)
const messages = [fileMessage]
const onSendPress = jest.fn()
const onFilePress = jest.fn()
const { getByLabelText } = render(
<Chat
onFilePress={onFilePress}
messages={messages}
onSendPress={onSendPress}
user={user}
/>
)
const button = getByLabelText('Open a file')
fireEvent.press(button)
expect(onFilePress).toHaveBeenCalledWith(fileMessage)
})
})

View File

@@ -0,0 +1,42 @@
import * as React from 'react'
import { Image, Text, TouchableOpacity, View } from 'react-native'
import { MessageType } from '../../types'
import { formatBytes, UserContext } from '../../utils'
import styles from './styles'
export interface FileMessageProps {
message: MessageType.File
onPress?: (file: MessageType.File) => void
}
export const FileMessage = ({ message, onPress }: FileMessageProps) => {
const user = React.useContext(UserContext)
const { container, iconContainer, name, size, textContainer } = styles({
message,
user,
})
const handlePress = () => {
onPress?.(message)
}
return (
<TouchableOpacity
accessibilityRole='button'
accessibilityLabel='Open a file'
onPress={handlePress}
>
<View style={container}>
<View style={iconContainer}>
<Image source={require('../../assets/icon-document.png')} />
</View>
<View style={textContainer}>
<Text accessibilityRole='text' style={name}>
{message.name}
</Text>
<Text style={size}>{formatBytes(message.size)}</Text>
</View>
</View>
</TouchableOpacity>
)
}

View File

@@ -0,0 +1 @@
export * from './FileMessage'

View File

@@ -0,0 +1,47 @@
import { StyleSheet } from 'react-native'
import { MessageType, User } from '../../types'
const styles = ({
message,
user,
}: {
message: MessageType.File
user?: User
}) =>
StyleSheet.create({
container: {
flexDirection: 'row',
},
iconContainer: {
alignItems: 'center',
backgroundColor:
user?.id === message.authorId ? '#ffffff33' : '#2e2c2c33',
borderRadius: 22,
height: 44,
justifyContent: 'center',
marginLeft: 16,
marginVertical: 16,
width: 44,
},
name: {
color: user?.id === message.authorId ? '#fff' : '#2e2c2c',
fontSize: 16,
fontWeight: '500',
lineHeight: 20,
},
size: {
color: user?.id === message.authorId ? '#ffffff66' : '#2e2c2c66',
fontSize: 12,
fontWeight: '500',
lineHeight: 16,
marginTop: 2,
},
textContainer: {
flexShrink: 1,
marginLeft: 12,
marginRight: 24,
marginVertical: 16,
},
})
export default styles

View File

@@ -5,7 +5,7 @@ import styles from './styles'
export interface ImageMessageProps {
message: MessageType.Image
onPress: (imageUrl: string) => void
onPress: (url: string) => void
}
export const ImageMessage = ({ message, onPress }: ImageMessageProps) => {
@@ -20,21 +20,21 @@ export const ImageMessage = ({ message, onPress }: ImageMessageProps) => {
React.useEffect(() => {
if (defaultHeight <= 0 || defaultWidth <= 0)
Image.getSize(
message.imageUrl,
message.url,
(width, height) => setSize({ height, width }),
() => setSize({ height: 0, width: 0 })
)
}, [defaultHeight, defaultWidth, message.imageUrl])
}, [defaultHeight, defaultWidth, message.url])
const handlePress = () => {
onPress(message.imageUrl)
onPress(message.url)
}
return (
<TouchableWithoutFeedback onPress={handlePress}>
<Image
accessibilityRole='image'
source={{ uri: message.imageUrl }}
source={{ uri: message.url }}
style={image}
/>
</TouchableWithoutFeedback>

View File

@@ -17,7 +17,7 @@ describe('text message', () => {
)
expect(getSizeMock).toHaveBeenCalledTimes(1)
const getSizeArgs = getSizeMock.mock.calls[0]
expect(getSizeArgs[0]).toBe(imageMessage.imageUrl)
expect(getSizeArgs[0]).toBe(imageMessage.url)
const success = getSizeArgs[1]
const error = getSizeArgs[2]
act(() => {
@@ -44,6 +44,6 @@ describe('text message', () => {
)
const button = getByRole('image').parent
fireEvent.press(button)
expect(onPress).toHaveBeenCalledWith(imageMessage.imageUrl)
expect(onPress).toHaveBeenCalledWith(imageMessage.url)
})
})

View File

@@ -7,15 +7,22 @@ import {
TextInputProps,
View,
} from 'react-native'
import { MessageType, SendImageCallback } from '../../types'
import {
MessageType,
SendAttachmentCallback,
SendAttachmentCallbackParams,
SendFileCallbackParams,
SendImageCallbackParams,
} from '../../types'
import { UserContext, uuidv4 } from '../../utils'
import { AttachmentButton } from '../AttachmentButton'
import { SendButton } from '../SendButton'
import styles from './styles'
export interface InputProps {
onAttachmentPress?: (send: SendImageCallback) => void
onAttachmentPress?: (sendAttachment: SendAttachmentCallback) => void
onContentBottomInsetUpdate?: (contentBottomInset: number) => void
onFilePress?: (file: MessageType.File) => void
onSendPress: (message: MessageType.Any) => void
panResponderPositionY?: Animated.Value
textInputProps?: TextInputProps
@@ -59,18 +66,33 @@ export const Input = ({
// TODO: This function is binded to the `onAttachmentPress`, how to mock this in tests?
/* istanbul ignore next */
const handleSendImage = ({
height,
imageUrl,
width,
}: Parameters<SendImageCallback>[0]) => {
onSendPress({
...defaultMessageParams(),
height,
imageUrl,
type: 'image',
width,
})
const handleSendAttachment = (params: SendAttachmentCallbackParams) => {
const isFileParams = (
arg: SendFileCallbackParams | SendImageCallbackParams
): arg is SendFileCallbackParams => {
return 'name' in arg
}
if (isFileParams(params)) {
const { mimeType, name, size, url } = params
onSendPress({
...defaultMessageParams(),
mimeType,
name,
size,
type: 'file',
url,
})
} else {
const { height, url, width } = params
onSendPress({
...defaultMessageParams(),
height,
type: 'image',
url,
width,
})
}
}
return (
@@ -82,7 +104,7 @@ export const Input = ({
<View style={styles.container}>
{user && (
<AttachmentButton
onPress={onAttachmentPress?.bind(null, handleSendImage)}
onPress={onAttachmentPress?.bind(null, handleSendAttachment)}
/>
)}
<TextInput

View File

@@ -2,19 +2,22 @@ import * as React from 'react'
import { View } from 'react-native'
import { MessageType, Size } from '../../types'
import { UserContext } from '../../utils'
import { FileMessage } from '../FileMessage'
import { ImageMessage } from '../ImageMessage'
import { TextMessage } from '../TextMessage'
import styles from './styles'
export interface MessageProps {
message: MessageType.Any
onImagePress: (imageUrl: string) => void
onFilePress?: (file: MessageType.File) => void
onImagePress: (url: string) => void
parentComponentSize: Size
previousMessageSameAuthor: boolean
}
export const Message = ({
message,
onFilePress,
onImagePress,
parentComponentSize,
previousMessageSameAuthor,
@@ -29,6 +32,8 @@ export const Message = ({
const renderMessage = () => {
switch (message.type) {
case 'file':
return <FileMessage message={message} onPress={onFilePress} />
case 'image':
return <ImageMessage message={message} onPress={onImagePress} />
case 'text':

View File

@@ -27,7 +27,6 @@ export const SendButton = ({
{...touchableOpacityProps}
onPress={handlePress}
>
{/* type-coverage:ignore-next-line */}
<Image source={require('../../assets/icon-send.png')} />
</TouchableOpacity>
)

View File

@@ -1,6 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace MessageType {
export type Any = Image | Text
export type Any = File | Image | Text
interface Base {
authorId: string
@@ -8,10 +8,18 @@ export namespace MessageType {
timestamp: number
}
export interface File extends Base {
mimeType?: string
name: string
size: number
type: 'file'
url: string
}
export interface Image extends Base {
height?: number
imageUrl: string
type: 'image'
url: string
width?: number
}
@@ -21,11 +29,30 @@ export namespace MessageType {
}
}
export type SendImageCallback = (payload: {
export type SendAttachmentCallback = (
payload: SendAttachmentCallbackParams
) => void
export type SendAttachmentCallbackParams =
| SendFileCallbackParams
| SendImageCallbackParams
export type SendFileCallback = (payload: SendFileCallbackParams) => void
export interface SendFileCallbackParams {
mimeType?: string
name: string
size: number
url: string
}
export type SendImageCallback = (payload: SendImageCallbackParams) => void
export interface SendImageCallbackParams {
height?: number
imageUrl: string
url: string
width?: number
}) => void
}
export interface Size {
height: number

View File

@@ -1,4 +1,16 @@
import { getTextSizeInBytes, uuidv4 } from '..'
import { formatBytes, getTextSizeInBytes, uuidv4 } from '..'
describe('formatBytes', () => {
it('formats bytes correctly when the size is 0', () => {
expect.assertions(1)
expect(formatBytes(0)).toStrictEqual('0 B')
})
it('formats bytes correctly', () => {
expect.assertions(1)
expect(formatBytes(1024)).toStrictEqual('1 kB')
})
})
describe('getTextSizeInBytes', () => {
it('calculates the size for a simple text', () => {

View File

@@ -4,6 +4,16 @@ import { User } from '../types'
export const UserContext = React.createContext<User | undefined>(undefined)
export const formatBytes = (size: number, fractionDigits = 2) => {
if (size === 0) return '0 B'
const multiple = Math.floor(Math.log(size) / Math.log(1024))
return (
parseFloat((size / Math.pow(1024, multiple)).toFixed(fractionDigits)) +
' ' +
['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][multiple]
)
}
export const getTextSizeInBytes = (text: string) => {
return new Blob([text]).size
}