mirror of https://github.com/tooot-app/app
Fxied #353
parent
99b38f421c
commit
e2ba4660df
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"javascript.inlayHints.functionLikeReturnTypes.enabled": false
|
||||
}
|
59
src/App.tsx
59
src/App.tsx
|
@ -74,37 +74,6 @@ const App: React.FC = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const children = useCallback(
|
||||
bootstrapped => {
|
||||
log('log', 'App', 'bootstrapped')
|
||||
if (bootstrapped) {
|
||||
log('log', 'App', 'loading actual app :)')
|
||||
const language = getSettingsLanguage(store.getState())
|
||||
if (!language) {
|
||||
store.dispatch(changeLanguage('en'))
|
||||
i18n.changeLanguage('en')
|
||||
} else {
|
||||
i18n.changeLanguage(language)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sentry.Native.TouchEventBoundary>
|
||||
<ActionSheetProvider>
|
||||
<AccessibilityManager>
|
||||
<ThemeManager>
|
||||
<Screens localCorrupt={localCorrupt} />
|
||||
</ThemeManager>
|
||||
</AccessibilityManager>
|
||||
</ActionSheetProvider>
|
||||
</Sentry.Native.TouchEventBoundary>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[localCorrupt]
|
||||
)
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
@ -112,7 +81,33 @@ const App: React.FC = () => {
|
|||
<PersistGate
|
||||
persistor={persistor}
|
||||
onBeforeLift={onBeforeLift}
|
||||
children={children}
|
||||
children={bootstrapped => {
|
||||
log('log', 'App', 'bootstrapped')
|
||||
if (bootstrapped) {
|
||||
log('log', 'App', 'loading actual app :)')
|
||||
const language = getSettingsLanguage(store.getState())
|
||||
if (!language) {
|
||||
store.dispatch(changeLanguage('en'))
|
||||
i18n.changeLanguage('en')
|
||||
} else {
|
||||
i18n.changeLanguage(language)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sentry.Native.TouchEventBoundary>
|
||||
<ActionSheetProvider>
|
||||
<AccessibilityManager>
|
||||
<ThemeManager>
|
||||
<Screens localCorrupt={localCorrupt} />
|
||||
</ThemeManager>
|
||||
</AccessibilityManager>
|
||||
</ActionSheetProvider>
|
||||
</Sentry.Native.TouchEventBoundary>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
|
|
|
@ -170,6 +170,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
}
|
||||
| { data: string | string[]; mimeType: string }
|
||||
) => {
|
||||
console.log('item', item)
|
||||
if (instanceActive < 0) {
|
||||
return
|
||||
}
|
||||
|
@ -253,6 +254,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
if (!text && !media.length) {
|
||||
return
|
||||
} else {
|
||||
console.log('media', media)
|
||||
navigationRef.navigate('Screen-Compose', { type: 'share', text, media })
|
||||
}
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
|||
import React, {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
@ -57,7 +58,7 @@ export interface Props {
|
|||
maxLength?: number
|
||||
}
|
||||
|
||||
const ComponentEmojis: React.FC<Props> = ({
|
||||
const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
|
||||
enabled = false,
|
||||
value,
|
||||
setValue,
|
||||
|
|
|
@ -27,18 +27,6 @@ const EmojisList = React.memo(
|
|||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||
const { colors } = useTheme()
|
||||
|
||||
const listHeader = useCallback(
|
||||
({ section: { title } }) => (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{ position: 'absolute', color: colors.secondary }}
|
||||
>
|
||||
{title}
|
||||
</CustomText>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const listItem = useCallback(
|
||||
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
||||
return (
|
||||
|
@ -112,7 +100,14 @@ const EmojisList = React.memo(
|
|||
keyboardShouldPersistTaps='always'
|
||||
sections={emojisState.emojis}
|
||||
keyExtractor={item => item[0].shortcode}
|
||||
renderSectionHeader={listHeader}
|
||||
renderSectionHeader={({ section: { title } }) => (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{ position: 'absolute', color: colors.secondary }}
|
||||
>
|
||||
{title}
|
||||
</CustomText>
|
||||
)}
|
||||
renderItem={listItem}
|
||||
windowSize={4}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
|
@ -81,10 +80,6 @@ const Input: React.FC<Props> = ({
|
|||
}
|
||||
: { start: 0, end: 0 }
|
||||
)
|
||||
const onSelectionChange = useCallback(
|
||||
({ nativeEvent: { selection } }) => (selectionRange.current = selection),
|
||||
[]
|
||||
)
|
||||
|
||||
const [inputFocused, setInputFocused] = useState(false)
|
||||
useEffect(() => {
|
||||
|
@ -128,7 +123,9 @@ const Input: React.FC<Props> = ({
|
|||
: undefined
|
||||
}}
|
||||
onChangeText={setValue}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onSelectionChange={({ nativeEvent: { selection } }) =>
|
||||
(selectionRange.current = selection)
|
||||
}
|
||||
value={value}
|
||||
{...(multiline && {
|
||||
multiline,
|
||||
|
|
|
@ -88,21 +88,6 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
}
|
||||
}, [domain])
|
||||
|
||||
const onSubmitEditing = useCallback(
|
||||
({ nativeEvent: { text } }) => {
|
||||
analytics('instance_textinput_submit', { match: text === domain })
|
||||
if (
|
||||
text === domain &&
|
||||
instanceQuery.isSuccess &&
|
||||
instanceQuery.data &&
|
||||
instanceQuery.data.uri
|
||||
) {
|
||||
processUpdate()
|
||||
}
|
||||
},
|
||||
[domain, instanceQuery.isSuccess, instanceQuery.data]
|
||||
)
|
||||
|
||||
const requestAuth = useMemo(() => {
|
||||
if (
|
||||
domain &&
|
||||
|
@ -180,7 +165,17 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
clearButtonMode='never'
|
||||
keyboardType='url'
|
||||
textContentType='URL'
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
onSubmitEditing={({ nativeEvent: { text } }) => {
|
||||
analytics('instance_textinput_submit', { match: text === domain })
|
||||
if (
|
||||
text === domain &&
|
||||
instanceQuery.isSuccess &&
|
||||
instanceQuery.data &&
|
||||
instanceQuery.data.uri
|
||||
) {
|
||||
processUpdate()
|
||||
}
|
||||
}}
|
||||
placeholder={' ' + t('server.textInput.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
returnKeyType='go'
|
||||
|
|
|
@ -215,7 +215,7 @@ const ParseHTML = React.memo(
|
|||
}
|
||||
|
||||
const renderNodeCallback = useCallback(
|
||||
(node, index) =>
|
||||
(node: any, index: any) =>
|
||||
renderNode({
|
||||
routeParams: route.params,
|
||||
colors,
|
||||
|
@ -231,7 +231,7 @@ const ParseHTML = React.memo(
|
|||
}),
|
||||
[]
|
||||
)
|
||||
const textComponent = useCallback(({ children }) => {
|
||||
const textComponent = useCallback(({ children }: any) => {
|
||||
if (children) {
|
||||
return (
|
||||
<ParseEmojis
|
||||
|
@ -246,26 +246,24 @@ const ParseHTML = React.memo(
|
|||
}
|
||||
}, [])
|
||||
const rootComponent = useCallback(
|
||||
({ children }) => {
|
||||
({ children }: any) => {
|
||||
const { t } = useTranslation('componentParse')
|
||||
|
||||
const [expandAllow, setExpandAllow] = useState(false)
|
||||
const [expanded, setExpanded] = useState(highlighted)
|
||||
|
||||
const onTextLayout = useCallback(({ nativeEvent }) => {
|
||||
if (
|
||||
numberOfLines === 1 ||
|
||||
nativeEvent.lines.length >= numberOfLines + 5
|
||||
) {
|
||||
setExpandAllow(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={{ overflow: 'hidden' }}>
|
||||
<CustomText
|
||||
children={children}
|
||||
onTextLayout={onTextLayout}
|
||||
onTextLayout={({ nativeEvent }) => {
|
||||
if (
|
||||
numberOfLines === 1 ||
|
||||
nativeEvent.lines.length >= numberOfLines + 5
|
||||
) {
|
||||
setExpandAllow(true)
|
||||
}
|
||||
}}
|
||||
numberOfLines={
|
||||
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
|
||||
}
|
||||
|
|
|
@ -64,17 +64,6 @@ const Timeline: React.FC<Props> = ({
|
|||
? data.pages?.flatMap(page => [...page.body])
|
||||
: []
|
||||
|
||||
const ItemSeparatorComponent = useCallback(
|
||||
({ leadingItem }) =>
|
||||
queryKey[1].page === 'Toot' && queryKey[1].toot === leadingItem.id ? (
|
||||
<ComponentSeparator extraMarginLeft={0} />
|
||||
) : (
|
||||
<ComponentSeparator
|
||||
extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const onEndReached = useCallback(
|
||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||
[isFetchingNextPage]
|
||||
|
@ -151,7 +140,17 @@ const Timeline: React.FC<Props> = ({
|
|||
/>
|
||||
}
|
||||
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
|
||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||
ItemSeparatorComponent={({ leadingItem }) =>
|
||||
queryKey[1].page === 'Toot' && queryKey[1].toot === leadingItem.id ? (
|
||||
<ComponentSeparator extraMarginLeft={0} />
|
||||
) : (
|
||||
<ComponentSeparator
|
||||
extraMarginLeft={
|
||||
StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
maintainVisibleContentPosition={
|
||||
isFetching
|
||||
? {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native'
|
||||
import { FlatList, LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
|
@ -169,7 +169,7 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
|
||||
const arrowStage = useSharedValue(0)
|
||||
const onLayout = useCallback(
|
||||
({ nativeEvent }) => {
|
||||
({ nativeEvent }: LayoutChangeEvent) => {
|
||||
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
||||
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import analytics from '@components/analytics'
|
||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||
import { store } from '@root/store'
|
||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
||||
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'
|
||||
import * as ExpoImagePicker from 'expo-image-picker'
|
||||
import i18next from 'i18next'
|
||||
import { Alert, Linking, Platform } from 'react-native'
|
||||
import ImagePicker, {
|
||||
Image,
|
||||
ImageOrVideo
|
||||
} from 'react-native-image-crop-picker'
|
||||
import { Asset, launchImageLibrary } from 'react-native-image-picker'
|
||||
|
||||
export interface Props {
|
||||
mediaType?: 'photo' | 'video'
|
||||
|
@ -28,43 +21,7 @@ const mediaSelector = async ({
|
|||
maximum,
|
||||
indicateMaximum = false,
|
||||
showActionSheetWithOptions
|
||||
}: Props): Promise<({ uri: string } & Omit<ImageOrVideo, 'path'>)[]> => {
|
||||
const checkLibraryPermission = async (): Promise<boolean> => {
|
||||
const { status } =
|
||||
await ExpoImagePicker.requestMediaLibraryPermissionsAsync()
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
i18next.t('componentMediaSelector:library.alert.title'),
|
||||
i18next.t('componentMediaSelector:library.alert.message'),
|
||||
[
|
||||
{
|
||||
text: i18next.t('common:buttons.cancel'),
|
||||
style: 'cancel',
|
||||
onPress: () =>
|
||||
analytics('mediaSelector_nopermission', {
|
||||
action: 'cancel'
|
||||
})
|
||||
},
|
||||
{
|
||||
text: i18next.t(
|
||||
'componentMediaSelector:library.alert.buttons.settings'
|
||||
),
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
analytics('mediaSelector_nopermission', {
|
||||
action: 'settings'
|
||||
})
|
||||
Linking.openURL('app-settings:')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}: Props): Promise<Asset[]> => {
|
||||
const _maximum =
|
||||
maximum ||
|
||||
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
|
||||
|
@ -105,79 +62,30 @@ const mediaSelector = async ({
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
const selectImage = async () => {
|
||||
const images = await ImagePicker.openPicker({
|
||||
const images = await launchImageLibrary({
|
||||
mediaType: 'photo',
|
||||
includeExif: false,
|
||||
multiple: true,
|
||||
minFiles: 1,
|
||||
maxFiles: _maximum,
|
||||
smartAlbums: ['UserLibrary'],
|
||||
writeTempFile: false,
|
||||
loadingLabelText: ''
|
||||
}).catch(() => {})
|
||||
...(resize && { maxWidth: resize.width, maxHeight: resize.height }),
|
||||
includeBase64: false,
|
||||
includeExtra: false,
|
||||
selectionLimit: _maximum
|
||||
})
|
||||
|
||||
if (!images) {
|
||||
if (!images.assets) {
|
||||
return reject()
|
||||
}
|
||||
|
||||
// react-native-image-crop-picker may return HEIC as JPG that causes upload failure
|
||||
if (Platform.OS === 'ios') {
|
||||
for (const [index, image] of images.entries()) {
|
||||
if (image.mime === 'image/heic') {
|
||||
const converted = await manipulateAsync(image.sourceURL!, [], {
|
||||
base64: false,
|
||||
compress: 0.8,
|
||||
format: SaveFormat.JPEG
|
||||
})
|
||||
images[index] = {
|
||||
...images[index],
|
||||
sourceURL: converted.uri,
|
||||
mime: 'image/jpeg'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resize) {
|
||||
return resolve(
|
||||
images.map(image => ({
|
||||
...image,
|
||||
uri: image.sourceURL || `file://${image.path}`
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
const croppedImages: Image[] = []
|
||||
for (const image of images) {
|
||||
const croppedImage = await ImagePicker.openCropper({
|
||||
mediaType: 'photo',
|
||||
path: image.sourceURL || image.path,
|
||||
width: resize.width,
|
||||
height: resize.height,
|
||||
cropperChooseText: i18next.t('common:buttons.apply'),
|
||||
cropperCancelText: i18next.t('common:buttons.cancel'),
|
||||
hideBottomControls: true
|
||||
}).catch(() => {})
|
||||
croppedImage && croppedImages.push(croppedImage)
|
||||
}
|
||||
return resolve(
|
||||
croppedImages.map(image => ({
|
||||
...image,
|
||||
uri: `file://${image.path}`
|
||||
}))
|
||||
)
|
||||
}
|
||||
return resolve(images.assets)
|
||||
}
|
||||
const selectVideo = async () => {
|
||||
const video = await ImagePicker.openPicker({
|
||||
const video = await launchImageLibrary({
|
||||
mediaType: 'video',
|
||||
includeExif: false,
|
||||
loadingLabelText: ''
|
||||
}).catch(() => {})
|
||||
includeBase64: false,
|
||||
includeExtra: false,
|
||||
selectionLimit: 1
|
||||
})
|
||||
|
||||
if (video) {
|
||||
return resolve([
|
||||
{ ...video, uri: video.sourceURL || `file://${video.path}` }
|
||||
])
|
||||
if (video.assets?.[0]) {
|
||||
return resolve(video.assets)
|
||||
} else {
|
||||
return reject()
|
||||
}
|
||||
|
@ -189,10 +97,6 @@ const mediaSelector = async ({
|
|||
cancelButtonIndex: mediaType ? 1 : 2
|
||||
},
|
||||
async buttonIndex => {
|
||||
if (!(await checkLibraryPermission())) {
|
||||
return reject()
|
||||
}
|
||||
|
||||
switch (mediaType) {
|
||||
case 'photo':
|
||||
if (buttonIndex === 0) {
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
import { store } from '@root/store'
|
||||
import { getInstanceConfigurationMediaAttachments } from '@utils/slices/instancesSlice'
|
||||
import { Action, manipulateAsync, SaveFormat } from 'expo-image-manipulator'
|
||||
import i18next from 'i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import ImagePicker from 'react-native-image-crop-picker'
|
||||
|
||||
export interface Props {
|
||||
type: 'image' | 'video'
|
||||
uri: string // This can be pure path or uri starting with file://
|
||||
mime?: string
|
||||
transform: {
|
||||
imageFormat?: SaveFormat.JPEG | SaveFormat.PNG
|
||||
resize?: boolean
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
}
|
||||
|
||||
const getFileExtension = (uri: string) => {
|
||||
const extension = uri.split('.').pop()
|
||||
// Using mime type standard of jpeg
|
||||
return extension === 'jpg' ? 'jpeg' : extension
|
||||
}
|
||||
|
||||
const mediaTransformation = async ({
|
||||
type,
|
||||
uri,
|
||||
mime,
|
||||
transform
|
||||
}: Props): Promise<{
|
||||
uri: string
|
||||
mime: string
|
||||
width: number
|
||||
height: number
|
||||
}> => {
|
||||
const configurationMediaAttachments =
|
||||
getInstanceConfigurationMediaAttachments(store.getState())
|
||||
|
||||
const fileExtension = getFileExtension(uri)
|
||||
|
||||
switch (type) {
|
||||
case 'image':
|
||||
if (mime === 'image/gif' || fileExtension === 'gif') {
|
||||
return Promise.reject('GIFs should not be transformed')
|
||||
}
|
||||
let targetFormat: SaveFormat.JPEG | SaveFormat.PNG = SaveFormat.JPEG
|
||||
|
||||
const supportedImageTypes =
|
||||
configurationMediaAttachments.supported_mime_types.filter(mime =>
|
||||
mime.startsWith('image/')
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
const transformations: Action[] = [
|
||||
!transform.resize && (transform.width || transform.height)
|
||||
? {
|
||||
resize: { width: transform.width, height: transform.height }
|
||||
}
|
||||
: null
|
||||
].filter(t => !!t)
|
||||
|
||||
if (mime) {
|
||||
if (
|
||||
mime !== `image/${fileExtension}` ||
|
||||
!supportedImageTypes.includes(mime)
|
||||
) {
|
||||
targetFormat = transform.imageFormat || SaveFormat.JPEG
|
||||
} else {
|
||||
targetFormat = mime.split('/').pop() as any
|
||||
}
|
||||
} else {
|
||||
if (!fileExtension) {
|
||||
return Promise.reject('Unable to get file extension')
|
||||
}
|
||||
if (!supportedImageTypes.includes(`image/${fileExtension}`)) {
|
||||
targetFormat = transform.imageFormat || SaveFormat.JPEG
|
||||
} else {
|
||||
targetFormat = fileExtension as any
|
||||
}
|
||||
}
|
||||
|
||||
const converted = await manipulateAsync(uri, transformations, {
|
||||
base64: false,
|
||||
compress: Platform.OS === 'ios' ? 0.8 : 1,
|
||||
format: targetFormat
|
||||
})
|
||||
|
||||
if (transform.resize) {
|
||||
const resized = await ImagePicker.openCropper({
|
||||
mediaType: 'photo',
|
||||
path: converted.uri,
|
||||
width: transform.width,
|
||||
height: transform.height,
|
||||
cropperChooseText: i18next.t('common:buttons.apply'),
|
||||
cropperCancelText: i18next.t('common:buttons.cancel'),
|
||||
hideBottomControls: true
|
||||
})
|
||||
if (!resized) {
|
||||
return Promise.reject('Resize failed')
|
||||
} else {
|
||||
return {
|
||||
uri: resized.path,
|
||||
mime: resized.mime,
|
||||
width: resized.width,
|
||||
height: resized.height
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uri: converted.uri,
|
||||
mime: transform.imageFormat || SaveFormat.JPEG,
|
||||
width: converted.width,
|
||||
height: converted.height
|
||||
}
|
||||
}
|
||||
case 'video':
|
||||
const supportedVideoTypes =
|
||||
configurationMediaAttachments.supported_mime_types.filter(mime =>
|
||||
mime.startsWith('video/')
|
||||
)
|
||||
|
||||
if (mime) {
|
||||
if (mime !== `video/${fileExtension}`) {
|
||||
console.warn('Video mime type and file extension does not match')
|
||||
}
|
||||
if (!supportedVideoTypes.includes(mime)) {
|
||||
return Promise.reject('Video file type is not supported')
|
||||
}
|
||||
} else {
|
||||
if (!fileExtension) {
|
||||
return Promise.reject('Unable to get file extension')
|
||||
}
|
||||
if (!supportedVideoTypes.includes(`video/${fileExtension}`)) {
|
||||
return Promise.reject('Video file type is not supported')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uri: uri,
|
||||
mime: mime || `video/${fileExtension}`,
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export default mediaTransformation
|
|
@ -15,8 +15,15 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { FormattedRelativeTime } from 'react-intl'
|
||||
import { Dimensions, Platform, Pressable, StyleSheet, View } from 'react-native'
|
||||
import {
|
||||
Dimensions,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
||||
|
@ -92,9 +99,7 @@ const ScreenAnnouncements: React.FC<
|
|||
>
|
||||
<Trans
|
||||
i18nKey='screenAnnouncements:content.published'
|
||||
components={[
|
||||
<RelativeTime time={item.published_at} />
|
||||
]}
|
||||
components={[<RelativeTime time={item.published_at} />]}
|
||||
/>
|
||||
</CustomText>
|
||||
<ScrollView
|
||||
|
@ -218,7 +223,7 @@ const ScreenAnnouncements: React.FC<
|
|||
contentOffset: { x },
|
||||
layoutMeasurement: { width }
|
||||
}
|
||||
}) => {
|
||||
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
setIndex(Math.floor(x / width))
|
||||
},
|
||||
[]
|
||||
|
|
|
@ -150,7 +150,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||
for (const m of params.media) {
|
||||
uploadAttachment({
|
||||
composeDispatch,
|
||||
media: { ...m, width: 100, height: 100 }
|
||||
media: { uri: m.uri, fileName: 'temp.jpg', type: m.mime }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,10 +47,6 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
|
||||
const [checkingAttachments, setCheckingAttachments] = useState(false)
|
||||
|
||||
const removeDraft = useCallback(ts => {
|
||||
dispatch(removeInstanceDraft(ts))
|
||||
}, [])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: ComposeStateDraft }) => {
|
||||
return (
|
||||
|
@ -144,7 +140,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
}}
|
||||
source={{
|
||||
uri:
|
||||
attachment.local?.local_thumbnail ||
|
||||
attachment.local?.thumbnail ||
|
||||
attachment.remote?.preview_url
|
||||
}}
|
||||
/>
|
||||
|
@ -157,38 +153,6 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
},
|
||||
[theme]
|
||||
)
|
||||
const renderHiddenItem = useCallback(
|
||||
({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: colors.red
|
||||
}}
|
||||
children={
|
||||
<Pressable
|
||||
style={{
|
||||
flexBasis:
|
||||
StyleConstants.Font.Size.L +
|
||||
StyleConstants.Spacing.Global.PagePadding * 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onPress={() => removeDraft(item.timestamp)}
|
||||
children={
|
||||
<Icon
|
||||
name='Trash'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={colors.primaryOverlay}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -220,7 +184,35 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
<SwipeListView
|
||||
data={instanceDrafts}
|
||||
renderItem={renderItem}
|
||||
renderHiddenItem={renderHiddenItem}
|
||||
renderHiddenItem={({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: colors.red
|
||||
}}
|
||||
children={
|
||||
<Pressable
|
||||
style={{
|
||||
flexBasis:
|
||||
StyleConstants.Font.Size.L +
|
||||
StyleConstants.Spacing.Global.PagePadding * 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
|
||||
children={
|
||||
<Icon
|
||||
name='Trash'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={colors.primaryOverlay}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
disableRightSwipe={true}
|
||||
rightOpenValue={-actionWidth}
|
||||
// previewRowKey={
|
||||
|
|
|
@ -35,7 +35,7 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
|
|||
video.local
|
||||
? ({
|
||||
url: video.local.uri,
|
||||
preview_url: video.local.local_thumbnail,
|
||||
preview_url: video.local.thumbnail,
|
||||
blurhash: video.remote?.blurhash
|
||||
} as Mastodon.AttachmentVideo)
|
||||
: (video.remote as Mastodon.AttachmentVideo)
|
||||
|
|
|
@ -4,13 +4,7 @@ import { useSearchQuery } from '@utils/queryHooks/search'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
} from 'react'
|
||||
import React, { useContext, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
findNodeHandle,
|
||||
|
@ -147,35 +141,25 @@ const ComposeRoot = React.memo(
|
|||
}
|
||||
}, [isFetching])
|
||||
|
||||
const listItem = useCallback(
|
||||
({ item }) => (
|
||||
<ComposeRootSuggestion
|
||||
item={item}
|
||||
composeState={composeState}
|
||||
composeDispatch={composeDispatch}
|
||||
/>
|
||||
),
|
||||
[composeState]
|
||||
)
|
||||
|
||||
const ListFooter = useCallback(
|
||||
() => (
|
||||
<ComposeRootFooter
|
||||
accessibleRefAttachments={accessibleRefAttachments}
|
||||
accessibleRefEmojis={accessibleRefEmojis}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<FlatList
|
||||
renderItem={listItem}
|
||||
renderItem={({ item }) => (
|
||||
<ComposeRootSuggestion
|
||||
item={item}
|
||||
composeState={composeState}
|
||||
composeDispatch={composeDispatch}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={listEmpty}
|
||||
keyboardShouldPersistTaps='always'
|
||||
ListHeaderComponent={ComposeRootHeader}
|
||||
ListFooterComponent={ListFooter}
|
||||
ListFooterComponent={() => (
|
||||
<ComposeRootFooter
|
||||
accessibleRefAttachments={accessibleRefAttachments}
|
||||
accessibleRefEmojis={accessibleRefEmojis}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
// @ts-ignore
|
||||
data={data ? data[composeState.tag?.type] : undefined}
|
||||
|
|
|
@ -56,9 +56,12 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||
})
|
||||
}, [composeState.attachments.sensitive])
|
||||
|
||||
const calculateWidth = useCallback(item => {
|
||||
const calculateWidth = useCallback((item: ExtendedAttachment) => {
|
||||
if (item.local) {
|
||||
return (item.local.width / item.local.height) * DEFAULT_HEIGHT
|
||||
return (
|
||||
((item.local.width || 100) / (item.local.height || 100)) *
|
||||
DEFAULT_HEIGHT
|
||||
)
|
||||
} else {
|
||||
if (item.remote) {
|
||||
if (item.remote.meta.original.aspect) {
|
||||
|
@ -135,7 +138,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||
<FastImage
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
source={{
|
||||
uri: item.local?.local_thumbnail || item.remote?.preview_url
|
||||
uri: item.local?.thumbnail || item.remote?.preview_url
|
||||
}}
|
||||
/>
|
||||
{item.remote?.meta?.original?.duration ? (
|
||||
|
|
|
@ -42,22 +42,6 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
|||
}
|
||||
}, [composeState.emoji.active])
|
||||
|
||||
const listHeader = useCallback(
|
||||
({ section: { title } }) => (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: StyleConstants.Spacing.L,
|
||||
color: colors.secondary
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</CustomText>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const listItem = useCallback(
|
||||
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
||||
return (
|
||||
|
@ -155,7 +139,18 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
|||
keyboardShouldPersistTaps='always'
|
||||
sections={composeState.emoji.emojis || []}
|
||||
keyExtractor={item => item[0].shortcode}
|
||||
renderSectionHeader={listHeader}
|
||||
renderSectionHeader={({ section: { title } }) => (
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: StyleConstants.Spacing.L,
|
||||
color: colors.secondary
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</CustomText>
|
||||
)}
|
||||
renderItem={listItem}
|
||||
windowSize={2}
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
|||
import i18next from 'i18next'
|
||||
import apiInstance from '@api/instance'
|
||||
import mediaSelector from '@components/mediaSelector'
|
||||
import { ImageOrVideo } from 'react-native-image-crop-picker'
|
||||
import { Asset } from 'react-native-image-picker'
|
||||
|
||||
export interface Props {
|
||||
composeDispatch: Dispatch<ComposeAction>
|
||||
|
@ -22,46 +22,40 @@ export const uploadAttachment = async ({
|
|||
media
|
||||
}: {
|
||||
composeDispatch: Dispatch<ComposeAction>
|
||||
media: { uri: string } & Pick<ImageOrVideo, 'mime' | 'width' | 'height'>
|
||||
media: Required<Pick<Asset, 'uri' | 'type' | 'fileName'>>
|
||||
}) => {
|
||||
const hash = await Crypto.digestStringAsync(
|
||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||
media.uri + Math.random()
|
||||
)
|
||||
|
||||
switch (media.mime.split('/')[0]) {
|
||||
switch (media.type.split('/')[0]) {
|
||||
case 'image':
|
||||
composeDispatch({
|
||||
type: 'attachment/upload/start',
|
||||
payload: {
|
||||
local: { ...media, type: 'image', local_thumbnail: media.uri, hash },
|
||||
local: { ...media, thumbnail: media.uri, hash },
|
||||
uploading: true
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'video':
|
||||
VideoThumbnails.getThumbnailAsync(media.uri)
|
||||
.then(({ uri, width, height }) =>
|
||||
.then(({ uri, width, height }) => {
|
||||
console.log('new', uri, width, height)
|
||||
composeDispatch({
|
||||
type: 'attachment/upload/start',
|
||||
payload: {
|
||||
local: {
|
||||
...media,
|
||||
type: 'video',
|
||||
local_thumbnail: uri,
|
||||
hash,
|
||||
width,
|
||||
height
|
||||
},
|
||||
local: { ...media, thumbnail: uri, hash, width, height },
|
||||
uploading: true
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(() =>
|
||||
composeDispatch({
|
||||
type: 'attachment/upload/start',
|
||||
payload: {
|
||||
local: { ...media, type: 'video', hash },
|
||||
local: { ...media, hash },
|
||||
uploading: true
|
||||
}
|
||||
})
|
||||
|
@ -71,7 +65,7 @@ export const uploadAttachment = async ({
|
|||
composeDispatch({
|
||||
type: 'attachment/upload/start',
|
||||
payload: {
|
||||
local: { ...media, type: 'unknown', hash },
|
||||
local: { ...media, hash },
|
||||
uploading: true
|
||||
}
|
||||
})
|
||||
|
@ -102,8 +96,8 @@ export const uploadAttachment = async ({
|
|||
const formData = new FormData()
|
||||
formData.append('file', {
|
||||
uri: media.uri,
|
||||
name: media.uri.match(new RegExp(/.*\/(.*)/))?.[1] || 'file.jpg',
|
||||
type: media.mime
|
||||
name: media.fileName,
|
||||
type: media.type
|
||||
} as any)
|
||||
|
||||
return apiInstance<Mastodon.Attachment>({
|
||||
|
@ -140,7 +134,8 @@ const chooseAndUploadAttachment = async ({
|
|||
showActionSheetWithOptions
|
||||
})
|
||||
for (const media of result) {
|
||||
uploadAttachment({ composeDispatch, media })
|
||||
const requiredMedia = media as Required<Asset>
|
||||
uploadAttachment({ composeDispatch, media: requiredMedia })
|
||||
await new Promise(res => setTimeout(res, 500))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,15 +101,7 @@ const ComposeTextInput: React.FC = () => {
|
|||
}
|
||||
|
||||
for (const file of files) {
|
||||
uploadAttachment({
|
||||
composeDispatch,
|
||||
media: {
|
||||
uri: file.uri,
|
||||
mime: file.type,
|
||||
width: 100,
|
||||
height: 100
|
||||
}
|
||||
})
|
||||
uploadAttachment({ composeDispatch, media: file })
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { ImageOrVideo } from 'react-native-image-crop-picker'
|
||||
import { Asset } from 'react-native-image-picker'
|
||||
|
||||
export type ExtendedAttachment = {
|
||||
remote?: Mastodon.Attachment
|
||||
local?: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'> & {
|
||||
type: 'image' | 'video' | 'unknown'
|
||||
local_thumbnail?: string
|
||||
hash?: string
|
||||
}
|
||||
local?: Asset & { thumbnail?: string; hash: string }
|
||||
uploading?: boolean
|
||||
}
|
||||
|
||||
|
@ -121,10 +117,7 @@ export type ComposeAction =
|
|||
}
|
||||
| {
|
||||
type: 'attachment/upload/end'
|
||||
payload: {
|
||||
remote: Mastodon.Attachment
|
||||
local: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'>
|
||||
}
|
||||
payload: { remote: Mastodon.Attachment; local: Asset }
|
||||
}
|
||||
| {
|
||||
type: 'attachment/upload/fail'
|
||||
|
|
|
@ -50,11 +50,9 @@ const usePanResponder = ({
|
|||
onLongPress,
|
||||
delayLongPress,
|
||||
onRequestClose
|
||||
}: Props): Readonly<[
|
||||
GestureResponderHandlers,
|
||||
Animated.Value,
|
||||
Animated.ValueXY
|
||||
]> => {
|
||||
}: Props): Readonly<
|
||||
[GestureResponderHandlers, Animated.Value, Animated.ValueXY]
|
||||
> => {
|
||||
let numberInitialTouches = 1
|
||||
let initialTouches: NativeTouchEvent[] = []
|
||||
let currentScale = initialScale
|
||||
|
@ -137,6 +135,7 @@ const usePanResponder = ({
|
|||
|
||||
if (gestureState.numberActiveTouches > 1) return
|
||||
|
||||
// @ts-ignore
|
||||
longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
|
||||
},
|
||||
onStart: (
|
||||
|
@ -150,6 +149,7 @@ const usePanResponder = ({
|
|||
|
||||
const tapTS = Date.now()
|
||||
!timer &&
|
||||
// @ts-ignore
|
||||
(timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
|
||||
// Handle double tap event by calculating diff between first and second taps timestamps
|
||||
|
||||
|
@ -158,6 +158,7 @@ const usePanResponder = ({
|
|||
)
|
||||
|
||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||
// @ts-ignore
|
||||
clearTimeout(timer)
|
||||
const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
|
||||
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
|
||||
|
@ -291,9 +292,8 @@ const usePanResponder = ({
|
|||
if (isTapGesture && currentScale > initialScale) {
|
||||
const { x, y } = currentTranslate
|
||||
const { dx, dy } = gestureState
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
||||
currentScale
|
||||
)
|
||||
const [topBound, leftBound, bottomBound, rightBound] =
|
||||
getBounds(currentScale)
|
||||
|
||||
let nextTranslateX = x + dx
|
||||
let nextTranslateY = y + dy
|
||||
|
@ -357,9 +357,8 @@ const usePanResponder = ({
|
|||
|
||||
if (tmpTranslate) {
|
||||
const { x, y } = tmpTranslate
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
||||
currentScale
|
||||
)
|
||||
const [topBound, leftBound, bottomBound, rightBound] =
|
||||
getBounds(currentScale)
|
||||
|
||||
let nextTranslateX = x
|
||||
let nextTranslateY = y
|
||||
|
|
|
@ -44,6 +44,7 @@ export const getImageStyles = (
|
|||
const transform = translate.getTranslateTransform()
|
||||
|
||||
if (scale) {
|
||||
// @ts-ignore
|
||||
transform.push({ scale }, { perspective: new Animated.Value(1000) })
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import GracefullyImage from '@components/GracefullyImage'
|
||||
import haptics from '@components/haptics'
|
||||
import Icon from '@components/Icon'
|
||||
import {
|
||||
BottomTabNavigationOptions,
|
||||
createBottomTabNavigator
|
||||
} from '@react-navigation/bottom-tabs'
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
import {
|
||||
RootStackScreenProps,
|
||||
|
@ -15,10 +12,7 @@ import {
|
|||
getInstanceAccount,
|
||||
getInstanceActive
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import {
|
||||
getVersionUpdate,
|
||||
retriveVersionLatest
|
||||
} from '@utils/slices/appSlice'
|
||||
import { getVersionUpdate, retriveVersionLatest } from '@utils/slices/appSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
|
@ -32,7 +26,7 @@ const Tab = createBottomTabNavigator<ScreenTabsStackParamList>()
|
|||
|
||||
const ScreenTabs = React.memo(
|
||||
({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const instanceActive = useSelector(getInstanceActive)
|
||||
const instanceAccount = useSelector(
|
||||
|
@ -40,57 +34,6 @@ const ScreenTabs = React.memo(
|
|||
(prev, next) => prev?.avatarStatic === next?.avatarStatic
|
||||
)
|
||||
|
||||
const screenOptions = useCallback(
|
||||
({ route }): BottomTabNavigationOptions => ({
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: colors.primaryDefault,
|
||||
tabBarInactiveTintColor: colors.secondary,
|
||||
tabBarShowLabel: false,
|
||||
...(Platform.OS === 'android' && { tabBarHideOnKeyboard: true }),
|
||||
tabBarStyle: { display: instanceActive !== -1 ? 'flex' : 'none' },
|
||||
tabBarIcon: ({
|
||||
focused,
|
||||
color,
|
||||
size
|
||||
}: {
|
||||
focused: boolean
|
||||
color: string
|
||||
size: number
|
||||
}) => {
|
||||
switch (route.name) {
|
||||
case 'Tab-Local':
|
||||
return <Icon name='Home' size={size} color={color} />
|
||||
case 'Tab-Public':
|
||||
return <Icon name='Globe' size={size} color={color} />
|
||||
case 'Tab-Compose':
|
||||
return <Icon name='Plus' size={size} color={color} />
|
||||
case 'Tab-Notifications':
|
||||
return <Icon name='Bell' size={size} color={color} />
|
||||
case 'Tab-Me':
|
||||
return (
|
||||
<GracefullyImage
|
||||
key={instanceAccount?.avatarStatic}
|
||||
uri={{ original: instanceAccount?.avatarStatic }}
|
||||
dimension={{
|
||||
width: size,
|
||||
height: size
|
||||
}}
|
||||
style={{
|
||||
borderRadius: size,
|
||||
overflow: 'hidden',
|
||||
borderWidth: focused ? 2 : 0,
|
||||
borderColor: focused ? colors.secondary : color
|
||||
}}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return <Icon name='AlertOctagon' size={size} color={color} />
|
||||
}
|
||||
}
|
||||
}),
|
||||
[instanceAccount?.avatarStatic, instanceActive, theme]
|
||||
)
|
||||
|
||||
const composeListeners = useMemo(
|
||||
() => ({
|
||||
tabPress: (e: any) => {
|
||||
|
@ -132,7 +75,53 @@ const ScreenTabs = React.memo(
|
|||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
|
||||
screenOptions={screenOptions}
|
||||
screenOptions={({ route }) => ({
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: colors.primaryDefault,
|
||||
tabBarInactiveTintColor: colors.secondary,
|
||||
tabBarShowLabel: false,
|
||||
...(Platform.OS === 'android' && { tabBarHideOnKeyboard: true }),
|
||||
tabBarStyle: { display: instanceActive !== -1 ? 'flex' : 'none' },
|
||||
tabBarIcon: ({
|
||||
focused,
|
||||
color,
|
||||
size
|
||||
}: {
|
||||
focused: boolean
|
||||
color: string
|
||||
size: number
|
||||
}) => {
|
||||
switch (route.name) {
|
||||
case 'Tab-Local':
|
||||
return <Icon name='Home' size={size} color={color} />
|
||||
case 'Tab-Public':
|
||||
return <Icon name='Globe' size={size} color={color} />
|
||||
case 'Tab-Compose':
|
||||
return <Icon name='Plus' size={size} color={color} />
|
||||
case 'Tab-Notifications':
|
||||
return <Icon name='Bell' size={size} color={color} />
|
||||
case 'Tab-Me':
|
||||
return (
|
||||
<GracefullyImage
|
||||
key={instanceAccount?.avatarStatic}
|
||||
uri={{ original: instanceAccount?.avatarStatic }}
|
||||