feat: mobile app package (#1075)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { SnIcon } from '@Root/Components/SnIcon'
|
||||
import { Text } from '@Screens/SideMenu/SideMenuCell.styled'
|
||||
import { hexToRGBA } from '@Style/Utils'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const uploadedFileItemStyles = StyleSheet.create({
|
||||
lockIcon: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
})
|
||||
|
||||
export const FileDataContainer = styled.View`
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
padding-top: 12px;
|
||||
`
|
||||
export const FileIconContainer = styled.View`
|
||||
margin-top: 2px;
|
||||
margin-right: 16px;
|
||||
`
|
||||
export const FileDetailsWithExtraIconsContainer = styled.View`
|
||||
flex-direction: row;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
border-bottom-color: ${({ theme }) => hexToRGBA(theme.stylekitBorderColor, 0.75)};
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
export const LockIconStyled = styled(SnIcon)`
|
||||
background-color: green;
|
||||
display: none;
|
||||
`
|
||||
export const FileDetailsContainer = styled.View`
|
||||
flex-shrink: 1;
|
||||
`
|
||||
export const FileName = styled(Text)`
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
`
|
||||
export const FileDateAndSizeContainer = styled.View`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
export const FileDateAndSize = styled.Text`
|
||||
color: ${({ theme }) => {
|
||||
return theme.stylekitForegroundColor
|
||||
}};
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
`
|
||||
@@ -0,0 +1,83 @@
|
||||
import { AppStackNavigationProp } from '@Root/AppStack'
|
||||
import { SnIcon } from '@Root/Components/SnIcon'
|
||||
import { useFiles } from '@Root/Hooks/useFiles'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { SCREEN_COMPOSE } from '@Root/Screens/screens'
|
||||
import { UploadedFileItemActionType } from '@Screens/UploadedFilesList/UploadedFileItemAction'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import React, { FC, useContext, useEffect, useState } from 'react'
|
||||
import { TouchableOpacity, View } from 'react-native'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import {
|
||||
FileDataContainer,
|
||||
FileDateAndSize,
|
||||
FileDateAndSizeContainer,
|
||||
FileDetailsContainer,
|
||||
FileDetailsWithExtraIconsContainer,
|
||||
FileIconContainer,
|
||||
FileName,
|
||||
uploadedFileItemStyles,
|
||||
} from './UploadedFileItem.styled'
|
||||
|
||||
export type UploadedFileItemProps = {
|
||||
file: FileItem
|
||||
note: SNNote
|
||||
isAttachedToNote: boolean
|
||||
}
|
||||
|
||||
export type TAppStackNavigationProp = AppStackNavigationProp<typeof SCREEN_COMPOSE>['navigation']
|
||||
|
||||
export const UploadedFileItem: FC<UploadedFileItemProps> = ({ file, note }) => {
|
||||
const application = useSafeApplicationContext()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { showActionsMenu, handleFileAction } = useFiles({ note })
|
||||
|
||||
const [fileName, setFileName] = useState(file.name)
|
||||
|
||||
useEffect(() => {
|
||||
setFileName(file.name)
|
||||
}, [file.name])
|
||||
|
||||
const iconType = application.iconsController.getIconForFileType(file.mimeType)
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
void handleFileAction({
|
||||
type: UploadedFileItemActionType.PreviewFile,
|
||||
payload: file,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => showActionsMenu(file)}
|
||||
>
|
||||
<View>
|
||||
<FileDataContainer>
|
||||
<FileIconContainer>
|
||||
<SnIcon type={iconType} width={32} height={32} />
|
||||
</FileIconContainer>
|
||||
<FileDetailsWithExtraIconsContainer>
|
||||
<FileDetailsContainer>
|
||||
<FileName>{fileName}</FileName>
|
||||
<FileDateAndSizeContainer>
|
||||
<FileDateAndSize>
|
||||
{file.created_at.toLocaleString()} · {formatSizeToReadableString(file.decryptedSize)}
|
||||
</FileDateAndSize>
|
||||
{file.protected && (
|
||||
<SnIcon
|
||||
type={'lock-filled'}
|
||||
width={12}
|
||||
height={12}
|
||||
fill={theme.stylekitPalSky}
|
||||
style={uploadedFileItemStyles.lockIcon}
|
||||
/>
|
||||
)}
|
||||
</FileDateAndSizeContainer>
|
||||
</FileDetailsContainer>
|
||||
</FileDetailsWithExtraIconsContainer>
|
||||
</FileDataContainer>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
|
||||
export enum UploadedFileItemActionType {
|
||||
AttachFileToNote,
|
||||
DetachFileToNote,
|
||||
DeleteFile,
|
||||
ShareFile,
|
||||
DownloadFile,
|
||||
RenameFile,
|
||||
ToggleFileProtection,
|
||||
PreviewFile,
|
||||
}
|
||||
|
||||
export type UploadedFileItemAction = {
|
||||
type: UploadedFileItemActionType
|
||||
payload: FileItem
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const useUploadedFilesListStyles = () => {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return StyleSheet.create({
|
||||
centeredView: {
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerTabContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
noAttachmentsIconContainer: {
|
||||
alignItems: 'center',
|
||||
marginTop: 24,
|
||||
},
|
||||
noAttachmentsIcon: {
|
||||
marginTop: 24,
|
||||
marginBottom: 24,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const UploadFilesListContainer = styled.View`
|
||||
margin-top: 12px;
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
export const HeaderTabItem = styled.View<{
|
||||
isActive: boolean
|
||||
isLeftTab?: boolean
|
||||
}>`
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
flex-grow: 1;
|
||||
background-color: ${({ theme, isActive }) => {
|
||||
return isActive ? theme.stylekitInfoColor : theme.stylekitInfoContrastColor
|
||||
}};
|
||||
border-width: 1px;
|
||||
border-color: ${({ theme }) => theme.stylekitInfoColor};
|
||||
border-top-right-radius: ${({ isLeftTab }) => (isLeftTab ? 0 : '8px')};
|
||||
border-bottom-right-radius: ${({ isLeftTab }) => (isLeftTab ? 0 : '8px')};
|
||||
border-top-left-radius: ${({ isLeftTab }) => (isLeftTab ? '8px' : 0)};
|
||||
border-bottom-left-radius: ${({ isLeftTab }) => (isLeftTab ? '8px' : 0)};
|
||||
margin-left: ${({ isLeftTab }) => (isLeftTab ? 0 : '-1px')};
|
||||
`
|
||||
export const TabText = styled.Text<{ isActive: boolean }>`
|
||||
font-weight: bold;
|
||||
color: ${({ isActive, theme }) => {
|
||||
return isActive ? theme.stylekitInfoContrastColor : theme.stylekitInfoColor
|
||||
}};
|
||||
`
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { SearchBar } from '@Root/Components/SearchBar'
|
||||
import { SnIcon } from '@Root/Components/SnIcon'
|
||||
import { useFiles } from '@Root/Hooks/useFiles'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_UPLOADED_FILES_LIST } from '@Root/Screens/screens'
|
||||
import { UploadedFileItem } from '@Root/Screens/UploadedFilesList/UploadedFileItem'
|
||||
import {
|
||||
HeaderTabItem,
|
||||
TabText,
|
||||
UploadFilesListContainer,
|
||||
useUploadedFilesListStyles,
|
||||
} from '@Root/Screens/UploadedFilesList/UploadedFilesList.styled'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { ICON_ATTACH } from '@Style/Icons'
|
||||
import { ThemeService } from '@Style/ThemeService'
|
||||
import React, { FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FlatList, ListRenderItem, Text, View } from 'react-native'
|
||||
import FAB from 'react-native-fab'
|
||||
import IosSearchBar from 'react-native-search-bar'
|
||||
import AndroidSearchBar from 'react-native-search-box'
|
||||
import Icon from 'react-native-vector-icons/Ionicons'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export enum Tabs {
|
||||
AttachedFiles,
|
||||
AllFiles,
|
||||
}
|
||||
|
||||
type Props = ModalStackNavigationProp<typeof SCREEN_UPLOADED_FILES_LIST>
|
||||
|
||||
export const UploadedFilesList: FC<Props> = props => {
|
||||
const { AttachedFiles, AllFiles } = Tabs
|
||||
const { note } = props.route.params
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const styles = useUploadedFilesListStyles()
|
||||
const navigation = useNavigation()
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(AllFiles)
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [filesListScrolled, setFilesListScrolled] = useState(false)
|
||||
|
||||
const iosSearchBarInputRef = useRef<IosSearchBar>(null)
|
||||
const androidSearchBarInputRef = useRef<typeof AndroidSearchBar>(null)
|
||||
const filesListRef = useRef<FlatList>(null)
|
||||
|
||||
const { attachedFiles, allFiles, handlePressAttachFile } = useFiles({
|
||||
note,
|
||||
})
|
||||
|
||||
const filesList = currentTab === Tabs.AttachedFiles ? attachedFiles : allFiles
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return searchString
|
||||
? filesList.filter(file => file.name.toLowerCase().includes(searchString.toLowerCase()))
|
||||
: filesList
|
||||
}, [filesList, searchString])
|
||||
|
||||
useEffect(() => {
|
||||
let screenTitle = 'Files'
|
||||
if (searchString) {
|
||||
const filesCount = filteredList.length
|
||||
screenTitle = `${filesCount} search result${filesCount !== 1 ? 's' : ''}`
|
||||
}
|
||||
navigation.setOptions({
|
||||
title: screenTitle,
|
||||
})
|
||||
}, [filteredList.length, navigation, searchString])
|
||||
|
||||
const scrollListToTop = useCallback(() => {
|
||||
if (filesListScrolled && filteredList.length > 0) {
|
||||
filesListRef.current?.scrollToIndex({ animated: false, index: 0 })
|
||||
setFilesListScrolled(false)
|
||||
}
|
||||
}, [filesListScrolled, filteredList.length])
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(textToSearch: string) => {
|
||||
setSearchString(textToSearch)
|
||||
scrollListToTop()
|
||||
},
|
||||
[scrollListToTop],
|
||||
)
|
||||
|
||||
const { centeredView, header, headerTabContainer, noAttachmentsIcon, noAttachmentsIconContainer } = styles
|
||||
|
||||
const onScroll = () => {
|
||||
if (filesListScrolled) {
|
||||
return
|
||||
}
|
||||
setFilesListScrolled(true)
|
||||
}
|
||||
|
||||
const renderItem: ListRenderItem<FileItem> = ({ item }) => {
|
||||
return <UploadedFileItem key={item.uuid} file={item} note={note} isAttachedToNote={attachedFiles.includes(item)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={centeredView}>
|
||||
<UploadFilesListContainer>
|
||||
<View style={header}>
|
||||
<View style={headerTabContainer}>
|
||||
<HeaderTabItem
|
||||
isActive={currentTab === AttachedFiles}
|
||||
isLeftTab={true}
|
||||
onTouchEnd={() => setCurrentTab(AttachedFiles)}
|
||||
>
|
||||
<TabText isActive={currentTab === AttachedFiles}>Attached</TabText>
|
||||
</HeaderTabItem>
|
||||
<HeaderTabItem isActive={currentTab === AllFiles} onTouchEnd={() => setCurrentTab(AllFiles)}>
|
||||
<TabText isActive={currentTab === AllFiles}>All files</TabText>
|
||||
</HeaderTabItem>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<SearchBar
|
||||
onChangeText={handleFilter}
|
||||
onSearchCancel={() => handleFilter('')}
|
||||
iosSearchBarInputRef={iosSearchBarInputRef}
|
||||
androidSearchBarInputRef={androidSearchBarInputRef}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{filteredList.length > 0 ? (
|
||||
<FlatList
|
||||
ref={filesListRef}
|
||||
data={filteredList}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.uuid}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
) : (
|
||||
<View style={noAttachmentsIconContainer}>
|
||||
<SnIcon type={'files-illustration'} style={noAttachmentsIcon} width={72} height={72} />
|
||||
<Text>{searchString ? 'No files found' : 'No files attached to this note'}</Text>
|
||||
</View>
|
||||
)}
|
||||
<FAB
|
||||
buttonColor={theme.stylekitInfoColor}
|
||||
iconTextColor={theme.stylekitInfoContrastColor}
|
||||
onClickAction={() => handlePressAttachFile(currentTab)}
|
||||
visible={true}
|
||||
size={30}
|
||||
iconTextComponent={<Icon name={ThemeService.nameForIcon(ICON_ATTACH)} />}
|
||||
/>
|
||||
</UploadFilesListContainer>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user