Skip to content
This repository has been archived by the owner on Jun 15, 2022. It is now read-only.

Commit

Permalink
feat: show progress on file upload and download (#608)
Browse files Browse the repository at this point in the history
  • Loading branch information
vardan-arm committed May 10, 2022
1 parent f1e707c commit 51d9707
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 43 deletions.
5 changes: 2 additions & 3 deletions ios/Podfile.lock
Expand Up @@ -387,15 +387,14 @@ PODS:
- React-Core
- RNPrivacySnapshot (1.0.0):
- React-Core
- RNReanimated (2.5.0):
- RNReanimated (2.8.0):
- DoubleConversion
- FBLazyVector
- FBReactNativeSpec
- glog
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- React
- React-callinvoker
- React-Core
- React-Core/DevSupport
Expand Down Expand Up @@ -742,7 +741,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 6e757e487a4834e7280e98e9bac66d2d9c575e9c
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023
RNReanimated: 190b6930d5d94832061278e1070bbe313e50c830
RNReanimated: 46cdb89ca59ab7181334f4ed05a70e82ddb36751
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSearchBar: 5ed8e13ba8a6c701fbd2afdfe4164493d24b2aee
RNShare: f116bbb04f310c665ca483d0bd1e88cf59b3b334
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -58,7 +58,7 @@
"react-native-keychain": "^8.0.0",
"react-native-mail": "standardnotes/react-native-mail#fd26119e67a2ffc5eaa95a9c17049743e39ce2d3",
"react-native-privacy-snapshot": "standardnotes/react-native-privacy-snapshot#653e904",
"react-native-reanimated": "^2.5.0",
"react-native-reanimated": "^2.8.0",
"react-native-safe-area-context": "^4.2.2",
"react-native-screens": "3.13.1",
"react-native-search-bar": "standardnotes/react-native-search-bar#7d2139d",
Expand Down
37 changes: 26 additions & 11 deletions src/Components/ToastWrapper.styled.ts
Expand Up @@ -2,15 +2,30 @@ import { StyleSheet } from 'react-native'
import { DefaultTheme } from 'styled-components/native'

export const useToastStyles = (theme: DefaultTheme) => {
return StyleSheet.create({
info: {
borderLeftColor: theme.stylekitInfoColor,
},
success: {
borderLeftColor: theme.stylekitSuccessColor,
},
error: {
borderLeftColor: theme.stylekitWarningColor,
},
})
return (props: { [key: string]: unknown }) => {
return StyleSheet.create({
info: {
borderLeftColor: theme.stylekitInfoColor,
height: props.percentComplete !== undefined ? 70 : 60,
},
animatedViewContainer: {
height: 8,
borderWidth: 1,
borderRadius: 8,
borderColor: theme.stylekitInfoColor,
marginRight: 8,
marginLeft: 12,
marginTop: -16,
},
animatedView: {
backgroundColor: theme.stylekitInfoColor,
},
success: {
borderLeftColor: theme.stylekitSuccessColor,
},
error: {
borderLeftColor: theme.stylekitWarningColor,
},
})
}
}
42 changes: 39 additions & 3 deletions src/Components/ToastWrapper.tsx
@@ -1,16 +1,52 @@
import { useToastStyles } from '@Components/ToastWrapper.styled'
import { useProgressBar } from '@Root/Hooks/useProgessBar'
import React, { FC, useContext } from 'react'
import { Animated, StyleSheet, View } from 'react-native'
import Toast, { ErrorToast, InfoToast, SuccessToast, ToastConfig } from 'react-native-toast-message'
import { ThemeContext } from 'styled-components'

export const ToastWrapper: FC = () => {
const theme = useContext(ThemeContext)
const styles = useToastStyles(theme)

const { updateProgressBar, progressBarWidth } = useProgressBar()

const toastStyles: ToastConfig = {
info: props => <InfoToast {...props} style={styles.info} />,
success: props => <SuccessToast {...props} style={styles.success} />,
error: props => <ErrorToast {...props} style={styles.error} />,
info: props => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)

return (
<View>
<InfoToast {...props} style={styles(props.props).info} />
{props.props?.percentComplete !== undefined ? (
<View style={[styles(props.props).animatedViewContainer]}>
<Animated.View
style={[
StyleSheet.absoluteFill,
{
...styles(props.props).animatedView,
width: progressBarWidth,
},
]}
/>
</View>
) : null}
</View>
)
},
success: props => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)

return <SuccessToast {...props} style={styles(props.props).success} />
},
error: props => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)

return <ErrorToast {...props} style={styles(props.props).error} />
},
}

return <Toast config={toastStyles} />
Expand Down
104 changes: 87 additions & 17 deletions src/Hooks/useFiles.ts
@@ -1,3 +1,4 @@
import { ErrorMessage } from '@Lib/constants'
import { ToastType } from '@Lib/Types'
import { useNavigation } from '@react-navigation/native'
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
Expand All @@ -8,6 +9,7 @@ import {
UploadedFileItemActionType,
} from '@Root/Screens/UploadedFilesList/UploadedFileItemAction'
import { Tabs } from '@Screens/UploadedFilesList/UploadedFilesList'
import { FileDownloadProgress } from '@standardnotes/files/dist/Domain/Types/FileDownloadProgress'
import { ButtonType, ChallengeReason, ClientDisplayableError, ContentType, SNFile, SNNote } from '@standardnotes/snjs'
import { CustomActionSheetOption, useCustomActionSheet } from '@Style/CustomActionSheet'
import { useCallback, useEffect, useState } from 'react'
Expand Down Expand Up @@ -53,6 +55,7 @@ export const useFiles = ({ note }: Props) => {
const [allFiles, setAllFiles] = useState<SNFile[]>([])
const [isDownloading, setIsDownloading] = useState(false)

const { GeneralText } = ErrorMessage
const { Success, Info, Error } = ToastType

const filesService = application.getFilesService()
Expand All @@ -75,6 +78,55 @@ export const useFiles = ({ note }: Props) => {
}
}, [])

const showDownloadToastWithProgressBar = useCallback(
(percentComplete: number | undefined) => {
const percentCompleteFormatted = filesService.formatCompletedPercent(percentComplete)

Toast.show({
type: Info,
text1: `Downloading and decrypting file... (${percentCompleteFormatted}%)`,
props: {
percentComplete: percentCompleteFormatted,
},
autoHide: false,
})
},
[Info, filesService]
)

const showUploadToastWithProgressBar = useCallback(
(fileName: string, percentComplete: number | undefined) => {
const percentCompleteFormatted = filesService.formatCompletedPercent(percentComplete)

Toast.show({
type: Info,
text1: `Uploading "${fileName}"... (${percentCompleteFormatted}%)`,
autoHide: false,
props: {
percentComplete: percentCompleteFormatted,
},
})
},
[Info, filesService]
)

const resetProgressState = useCallback(() => {
Toast.show({
type: Info,
props: {
percentComplete: 0,
},
onShow: Toast.hide,
})
}, [Info])

const updateProgressPercentOnDownload = useCallback(
(progress: FileDownloadProgress | undefined) => {
showDownloadToastWithProgressBar(progress?.percentComplete)
},
[showDownloadToastWithProgressBar]
)

const downloadFileAndReturnLocalPath = useCallback(
async ({
file,
Expand All @@ -92,20 +144,26 @@ export const useFiles = ({ note }: Props) => {
setIsDownloading(true)

try {
Toast.show({
type: Info,
text1: 'Downloading and decrypting file...',
autoHide: false,
onPress: Toast.hide,
})
showDownloadToastWithProgressBar(0)

const path = filesService.getDestinationPath({
fileName: file.name,
saveInTempLocation,
})

await deleteFileAtPath(path)
await filesService.downloadFileInChunks(file, path)
const response = await filesService.downloadFileInChunks(file, path, updateProgressPercentOnDownload)

resetProgressState()

if (response instanceof ClientDisplayableError) {
Toast.show({
type: Error,
text1: 'Error',
text2: response.text || GeneralText,
})
return
}

if (showSuccessToast) {
Toast.show({
Expand Down Expand Up @@ -134,7 +192,17 @@ export const useFiles = ({ note }: Props) => {
setIsDownloading(false)
}
},
[Error, Info, Success, deleteFileAtPath, filesService, isDownloading]
[
Error,
GeneralText,
Success,
deleteFileAtPath,
filesService,
isDownloading,
resetProgressState,
showDownloadToastWithProgressBar,
updateProgressPercentOnDownload,
]
)

const cleanupTempFileOnAndroid = useCallback(
Expand Down Expand Up @@ -319,7 +387,8 @@ export const useFiles = ({ note }: Props) => {
if (response instanceof ClientDisplayableError) {
Toast.show({
type: Error,
text1: response.text,
text1: 'Error',
text2: response.text || GeneralText,
})
return
}
Expand All @@ -330,7 +399,7 @@ export const useFiles = ({ note }: Props) => {
})
}
},
[Error, Info, Success, application.alertService, application.files]
[Error, GeneralText, Info, Success, application.alertService, application.files]
)

const handlePickFilesError = async (error: unknown) => {
Expand Down Expand Up @@ -370,13 +439,6 @@ export const useFiles = ({ note }: Props) => {
const uploadSingleFile = async (file: DocumentPickerResponse | Asset, size: number): Promise<SNFile | void> => {
try {
const fileName = filesService.getFileName(file)

Toast.show({
type: Info,
text1: `Uploading "${fileName}"...`,
autoHide: false,
})

const operation = await application.files.beginNewFileUpload(size)

if (operation instanceof ClientDisplayableError) {
Expand All @@ -387,12 +449,20 @@ export const useFiles = ({ note }: Props) => {
return
}

const initialPercentComplete = operation.getProgress().percentComplete

showUploadToastWithProgressBar(fileName, initialPercentComplete)

const onChunk = async (chunk: Uint8Array, index: number, isLast: boolean) => {
await application.files.pushBytesForUpload(operation, chunk, index, isLast)
showUploadToastWithProgressBar(fileName, operation.getProgress().percentComplete)
}

const fileResult = await filesService.readFile(file, onChunk)
const fileObj = await application.files.finishUpload(operation, fileResult)

resetProgressState()

if (fileObj instanceof ClientDisplayableError) {
Toast.show({
type: Error,
Expand Down
28 changes: 28 additions & 0 deletions src/Hooks/useProgessBar.ts
@@ -0,0 +1,28 @@
import { useCallback, useRef } from 'react'
import { Animated } from 'react-native'

export const useProgressBar = () => {
const counterRef = useRef(new Animated.Value(0)).current

const updateProgressBar = useCallback(
(percentComplete: number) => {
Animated.timing(counterRef, {
toValue: percentComplete,
duration: 500,
useNativeDriver: false,
}).start()
},
[counterRef]
)

const progressBarWidth = counterRef.interpolate({
inputRange: [0, 100],
outputRange: ['0%', '100%'],
extrapolate: 'identity',
})

return {
updateProgressBar,
progressBarWidth,
}
}
16 changes: 14 additions & 2 deletions src/Lib/FilesService.ts
@@ -1,4 +1,6 @@
import { ByteChunker, FileSelectionResponse, OnChunkCallback } from '@standardnotes/filepicker'
import { FileDownloadProgress } from '@standardnotes/files/dist/Domain/Types/FileDownloadProgress'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ApplicationService, SNFile } from '@standardnotes/snjs'
import { Buffer } from 'buffer'
import { Base64 } from 'js-base64'
Expand Down Expand Up @@ -38,12 +40,18 @@ export class FilesService extends ApplicationService {
return false
}

async downloadFileInChunks(file: SNFile, path: string): Promise<void> {
await this.application.files.downloadFile(file, async (decryptedBytes: Uint8Array) => {
async downloadFileInChunks(
file: SNFile,
path: string,
handleOnChunk: (progress: FileDownloadProgress | undefined) => unknown
): Promise<ClientDisplayableError | undefined> {
const response = await this.application.files.downloadFile(file, async (decryptedBytes: Uint8Array, progress) => {
const base64String = new Buffer(decryptedBytes).toString('base64')
handleOnChunk(progress)

await RNFS.appendFile(path, base64String, 'base64')
})
return response
}

getFileName(file: DocumentPickerResponse | Asset) {
Expand Down Expand Up @@ -83,4 +91,8 @@ export class FilesService extends ApplicationService {
sortByName(file1: SNFile, file2: SNFile): number {
return file1.name.toLocaleLowerCase() > file2.name.toLocaleLowerCase() ? 1 : -1
}

formatCompletedPercent(percent: number | undefined) {
return Math.round(percent || 0)
}
}
3 changes: 3 additions & 0 deletions src/Lib/constants.ts
@@ -0,0 +1,3 @@
export enum ErrorMessage {
GeneralText = 'An error occurred. Please try again later.',
}

0 comments on commit 51d9707

Please sign in to comment.