mirror of
https://github.com/tooot-app/app
synced 2025-03-20 21:40:17 +01:00
Fxied #353
This commit is contained in:
parent
99b38f421c
commit
e2ba4660df
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -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 })
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
13
src/screens/Compose/utils/types.d.ts
vendored
13
src/screens/Compose/utils/types.d.ts
vendored
@ -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 }}
|
||||
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} />
|
||||
}
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Tab.Screen name='Tab-Local' component={TabLocal} />
|
||||
<Tab.Screen name='Tab-Public' component={TabPublic} />
|
||||
|
@ -44,16 +44,16 @@ const TabLocal = React.memo(
|
||||
)
|
||||
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
const children = useCallback(
|
||||
() => (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
lookback='Following'
|
||||
customProps={{ renderItem }}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
|
@ -1,16 +1,22 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabMeBookmarks = React.memo(
|
||||
() => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -1,19 +1,22 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineConversation from '@components/Timeline/Conversation'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabMeConversations = React.memo(
|
||||
() => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineConversation conversation={item} queryKey={queryKey} />
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineConversation conversation={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -1,17 +1,22 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabMeFavourites = React.memo(
|
||||
() => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
|
||||
route: {
|
||||
@ -10,12 +10,17 @@ const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
|
||||
}
|
||||
}) => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabMeListsList
|
||||
|
@ -43,14 +43,17 @@ const TabNotifications = React.memo(
|
||||
)
|
||||
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineNotifications notification={item} queryKey={queryKey} />
|
||||
),
|
||||
[]
|
||||
)
|
||||
const children = useCallback(
|
||||
() => <Timeline queryKey={queryKey} customProps={{ renderItem }} />,
|
||||
() => (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineNotifications notification={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -30,18 +30,10 @@ const TabSharedAccount: React.FC<
|
||||
|
||||
const scrollY = useSharedValue(0)
|
||||
|
||||
const onScroll = useCallback(({ nativeEvent }) => {
|
||||
scrollY.value = nativeEvent.contentOffset.y
|
||||
}, [])
|
||||
|
||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
||||
'Timeline',
|
||||
{ page: 'Account_Default', account: account.id }
|
||||
])
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
const isFetchingTimeline = useIsFetching(queryKey)
|
||||
const fetchedTimeline = useRef(false)
|
||||
useEffect(() => {
|
||||
@ -97,8 +89,11 @@ const TabSharedAccount: React.FC<
|
||||
queryKey={queryKey}
|
||||
disableRefresh
|
||||
customProps={{
|
||||
renderItem,
|
||||
onScroll,
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
),
|
||||
onScroll: ({ nativeEvent }) =>
|
||||
(scrollY.value = nativeEvent.contentOffset.y),
|
||||
ListHeaderComponent,
|
||||
maintainVisibleContentPosition: undefined
|
||||
}}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { Placeholder, Fade } from 'rn-placeholder'
|
||||
import AccountInformationAccount from './Information/Account'
|
||||
@ -19,21 +19,21 @@ export interface Props {
|
||||
|
||||
const AccountInformation = React.memo(
|
||||
({ account }: Props) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const { name } = useRoute()
|
||||
const myInfo = name !== 'Tab-Shared-Account'
|
||||
|
||||
const animation = useCallback(
|
||||
props => (
|
||||
<Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} />
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<Placeholder Animation={animation}>
|
||||
<Placeholder
|
||||
Animation={props => (
|
||||
<Fade
|
||||
{...props}
|
||||
style={{ backgroundColor: colors.shimmerHighlight }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<View style={styles.avatarAndActions}>
|
||||
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
||||
<AccountInformationActions account={account} myInfo={myInfo} />
|
||||
|
@ -2,11 +2,11 @@ import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabSharedAttachments: React.FC<TabSharedStackScreenProps<
|
||||
'Tab-Shared-Attachments'
|
||||
>> = ({
|
||||
const TabSharedAttachments: React.FC<
|
||||
TabSharedStackScreenProps<'Tab-Shared-Attachments'>
|
||||
> = ({
|
||||
route: {
|
||||
params: { account }
|
||||
}
|
||||
@ -15,11 +15,16 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<
|
||||
'Timeline',
|
||||
{ page: 'Account_Attachments', account: account.id }
|
||||
]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
}
|
||||
|
||||
export default TabSharedAttachments
|
||||
|
@ -2,21 +2,26 @@ import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<
|
||||
'Tab-Shared-Hashtag'
|
||||
>> = ({
|
||||
const TabSharedHashtag: React.FC<
|
||||
TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
|
||||
> = ({
|
||||
route: {
|
||||
params: { hashtag }
|
||||
}
|
||||
}) => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
return (
|
||||
<Timeline
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
}
|
||||
|
||||
export default TabSharedHashtag
|
||||
|
@ -139,65 +139,22 @@ const TabSharedSearch: React.FC<
|
||||
</View>
|
||||
)
|
||||
}, [status])
|
||||
const sectionHeader = useCallback(
|
||||
({ section: { translation } }) => (
|
||||
<View
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.M,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
{translation}
|
||||
</CustomText>
|
||||
</View>
|
||||
),
|
||||
|
||||
const listItem = useCallback(
|
||||
({ item, section }: { item: any; section: any }) => {
|
||||
switch (section.title) {
|
||||
case 'accounts':
|
||||
return <ComponentAccount account={item} origin='search' />
|
||||
case 'hashtags':
|
||||
return <ComponentHashtag hashtag={item} origin='search' />
|
||||
case 'statuses':
|
||||
return <TimelineDefault item={item} disableDetails origin='search' />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
const sectionFooter = useCallback(
|
||||
({ section: { data, translation } }) =>
|
||||
!data.length ? (
|
||||
<View
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.S,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{ textAlign: 'center', color: colors.secondary }}
|
||||
>
|
||||
<Trans
|
||||
i18nKey='screenTabs:shared.search.notFound'
|
||||
values={{ searchTerm: text, type: translation }}
|
||||
components={{
|
||||
bold: <CustomText fontWeight='Bold' />
|
||||
}}
|
||||
/>
|
||||
</CustomText>
|
||||
</View>
|
||||
) : null,
|
||||
[text]
|
||||
)
|
||||
const listItem = useCallback(({ item, section }) => {
|
||||
switch (section.title) {
|
||||
case 'accounts':
|
||||
return <ComponentAccount account={item} origin='search' />
|
||||
case 'hashtags':
|
||||
return <ComponentHashtag hashtag={item} origin='search' />
|
||||
case 'statuses':
|
||||
return <TimelineDefault item={item} disableDetails origin='search' />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
@ -211,8 +168,48 @@ const TabSharedSearch: React.FC<
|
||||
sections={data || []}
|
||||
ListEmptyComponent={listEmpty}
|
||||
keyboardShouldPersistTaps='always'
|
||||
renderSectionHeader={sectionHeader}
|
||||
renderSectionFooter={sectionFooter}
|
||||
renderSectionHeader={({ section: { translation } }) => (
|
||||
<View
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.M,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
{translation}
|
||||
</CustomText>
|
||||
</View>
|
||||
)}
|
||||
renderSectionFooter={({ section: { data, translation } }) =>
|
||||
!data.length ? (
|
||||
<View
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.S,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{ textAlign: 'center', color: colors.secondary }}
|
||||
>
|
||||
<Trans
|
||||
i18nKey='screenTabs:shared.search.notFound'
|
||||
values={{ searchTerm: text, type: translation }}
|
||||
components={{
|
||||
bold: <CustomText fontWeight='Bold' />
|
||||
}}
|
||||
/>
|
||||
</CustomText>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
keyExtractor={(item, index) => item + index}
|
||||
SectionSeparatorComponent={ComponentSeparator}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
|
@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { InfiniteQueryObserver, useQueryClient } from 'react-query'
|
||||
|
||||
@ -59,43 +59,35 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||
})
|
||||
}, [scrolled.current])
|
||||
|
||||
// Toot page auto scroll to selected toot
|
||||
const onScrollToIndexFailed = useCallback(
|
||||
error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
try {
|
||||
error.index < itemsLength &&
|
||||
setTimeout(
|
||||
() =>
|
||||
flRef.current?.scrollToIndex({
|
||||
index: error.index,
|
||||
viewOffset: 100
|
||||
}),
|
||||
500
|
||||
)
|
||||
} catch {}
|
||||
},
|
||||
[itemsLength]
|
||||
)
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineDefault
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
highlighted={toot.id === item.id}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
flRef={flRef}
|
||||
queryKey={queryKey}
|
||||
customProps={{ renderItem, onScrollToIndexFailed }}
|
||||
customProps={{
|
||||
renderItem: ({ item }) => (
|
||||
<TimelineDefault
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
highlighted={toot.id === item.id}
|
||||
/>
|
||||
),
|
||||
onScrollToIndexFailed: error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
try {
|
||||
error.index < itemsLength &&
|
||||
setTimeout(
|
||||
() =>
|
||||
flRef.current?.scrollToIndex({
|
||||
index: error.index,
|
||||
viewOffset: 100
|
||||
}),
|
||||
500
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
}}
|
||||
disableRefresh
|
||||
disableInfinity
|
||||
/>
|
||||
|
@ -9,28 +9,20 @@ import { FlatList } from 'react-native-gesture-handler'
|
||||
const TabSharedUsers = React.memo(
|
||||
({ route: { params } }: TabSharedStackScreenProps<'Tab-Shared-Users'>) => {
|
||||
const queryKey: QueryKeyUsers = ['Users', params]
|
||||
const {
|
||||
data,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useUsersQuery({
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage.links?.prev && { since_id: firstPage.links.next },
|
||||
getNextPageParam: lastPage =>
|
||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
||||
}
|
||||
})
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
useUsersQuery({
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage.links?.prev && { since_id: firstPage.links.next },
|
||||
getNextPageParam: lastPage =>
|
||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
||||
}
|
||||
})
|
||||
const flattenData = data?.pages
|
||||
? data.pages.flatMap(page => [...page.body])
|
||||
: []
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <ComponentAccount account={item} origin='relationship' />,
|
||||
[]
|
||||
)
|
||||
const onEndReached = useCallback(
|
||||
() => hasNextPage && !isFetchingNextPage && fetchNextPage(),
|
||||
[hasNextPage, isFetchingNextPage]
|
||||
@ -41,7 +33,9 @@ const TabSharedUsers = React.memo(
|
||||
windowSize={7}
|
||||
data={flattenData}
|
||||
style={styles.flatList}
|
||||
renderItem={renderItem}
|
||||
renderItem={({ item }) => (
|
||||
<ComponentAccount account={item} origin='relationship' />
|
||||
)}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={0.75}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
|
@ -1,4 +1,10 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react'
|
||||
import { AccessibilityInfo } from 'react-native'
|
||||
|
||||
type ContextType = {
|
||||
@ -15,7 +21,7 @@ const AccessibilityContext = createContext<ContextType>({
|
||||
|
||||
export const useAccessibility = () => useContext(AccessibilityContext)
|
||||
|
||||
const AccessibilityManager: React.FC = ({ children }) => {
|
||||
const AccessibilityManager: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false)
|
||||
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false)
|
||||
const [boldTextEnabled, setBoldTextEnabled] = useState(false)
|
||||
|
@ -1,4 +1,10 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react'
|
||||
import { Appearance } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ColorDefinitions, getColors, Theme } from '@utils/styles/themes'
|
||||
@ -74,7 +80,7 @@ const determineTheme = (
|
||||
}
|
||||
}
|
||||
|
||||
const ThemeManager: React.FC = ({ children }) => {
|
||||
const ThemeManager: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const osTheme = useColorSchemeDelay()
|
||||
const userTheme = useSelector(getSettingsTheme)
|
||||
const darkTheme = useSelector(getSettingsDarkTheme)
|
||||
|
Loading…
x
Reference in New Issue
Block a user