mirror of https://github.com/tooot-app/app
Fxied #353
This commit is contained in:
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 (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
@ -112,7 +81,33 @@ const App: React.FC = () => {
|
||||||
<PersistGate
|
<PersistGate
|
||||||
persistor={persistor}
|
persistor={persistor}
|
||||||
onBeforeLift={onBeforeLift}
|
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>
|
</Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
@ -170,6 +170,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
}
|
}
|
||||||
| { data: string | string[]; mimeType: string }
|
| { data: string | string[]; mimeType: string }
|
||||||
) => {
|
) => {
|
||||||
|
console.log('item', item)
|
||||||
if (instanceActive < 0) {
|
if (instanceActive < 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -253,6 +254,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
if (!text && !media.length) {
|
if (!text && !media.length) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
console.log('media', media)
|
||||||
navigationRef.navigate('Screen-Compose', { type: 'share', text, media })
|
navigationRef.navigate('Screen-Compose', { type: 'share', text, media })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
|
PropsWithChildren,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -57,7 +58,7 @@ export interface Props {
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentEmojis: React.FC<Props> = ({
|
const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
|
||||||
enabled = false,
|
enabled = false,
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
|
|
|
@ -27,18 +27,6 @@ const EmojisList = React.memo(
|
||||||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const listHeader = useCallback(
|
|
||||||
({ section: { title } }) => (
|
|
||||||
<CustomText
|
|
||||||
fontStyle='S'
|
|
||||||
style={{ position: 'absolute', color: colors.secondary }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</CustomText>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const listItem = useCallback(
|
const listItem = useCallback(
|
||||||
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -112,7 +100,14 @@ const EmojisList = React.memo(
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
sections={emojisState.emojis}
|
sections={emojisState.emojis}
|
||||||
keyExtractor={item => item[0].shortcode}
|
keyExtractor={item => item[0].shortcode}
|
||||||
renderSectionHeader={listHeader}
|
renderSectionHeader={({ section: { title } }) => (
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
style={{ position: 'absolute', color: colors.secondary }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</CustomText>
|
||||||
|
)}
|
||||||
renderItem={listItem}
|
renderItem={listItem}
|
||||||
windowSize={4}
|
windowSize={4}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
|
@ -81,10 +80,6 @@ const Input: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
: { start: 0, end: 0 }
|
: { start: 0, end: 0 }
|
||||||
)
|
)
|
||||||
const onSelectionChange = useCallback(
|
|
||||||
({ nativeEvent: { selection } }) => (selectionRange.current = selection),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [inputFocused, setInputFocused] = useState(false)
|
const [inputFocused, setInputFocused] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -128,7 +123,9 @@ const Input: React.FC<Props> = ({
|
||||||
: undefined
|
: undefined
|
||||||
}}
|
}}
|
||||||
onChangeText={setValue}
|
onChangeText={setValue}
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={({ nativeEvent: { selection } }) =>
|
||||||
|
(selectionRange.current = selection)
|
||||||
|
}
|
||||||
value={value}
|
value={value}
|
||||||
{...(multiline && {
|
{...(multiline && {
|
||||||
multiline,
|
multiline,
|
||||||
|
|
|
@ -88,21 +88,6 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
}, [domain])
|
}, [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(() => {
|
const requestAuth = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
domain &&
|
domain &&
|
||||||
|
@ -180,7 +165,17 @@ const ComponentInstance: React.FC<Props> = ({
|
||||||
clearButtonMode='never'
|
clearButtonMode='never'
|
||||||
keyboardType='url'
|
keyboardType='url'
|
||||||
textContentType='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')}
|
placeholder={' ' + t('server.textInput.placeholder')}
|
||||||
placeholderTextColor={colors.secondary}
|
placeholderTextColor={colors.secondary}
|
||||||
returnKeyType='go'
|
returnKeyType='go'
|
||||||
|
|
|
@ -215,7 +215,7 @@ const ParseHTML = React.memo(
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderNodeCallback = useCallback(
|
const renderNodeCallback = useCallback(
|
||||||
(node, index) =>
|
(node: any, index: any) =>
|
||||||
renderNode({
|
renderNode({
|
||||||
routeParams: route.params,
|
routeParams: route.params,
|
||||||
colors,
|
colors,
|
||||||
|
@ -231,7 +231,7 @@ const ParseHTML = React.memo(
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const textComponent = useCallback(({ children }) => {
|
const textComponent = useCallback(({ children }: any) => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return (
|
return (
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
|
@ -246,26 +246,24 @@ const ParseHTML = React.memo(
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
const rootComponent = useCallback(
|
const rootComponent = useCallback(
|
||||||
({ children }) => {
|
({ children }: any) => {
|
||||||
const { t } = useTranslation('componentParse')
|
const { t } = useTranslation('componentParse')
|
||||||
|
|
||||||
const [expandAllow, setExpandAllow] = useState(false)
|
const [expandAllow, setExpandAllow] = useState(false)
|
||||||
const [expanded, setExpanded] = useState(highlighted)
|
const [expanded, setExpanded] = useState(highlighted)
|
||||||
|
|
||||||
const onTextLayout = useCallback(({ nativeEvent }) => {
|
|
||||||
if (
|
|
||||||
numberOfLines === 1 ||
|
|
||||||
nativeEvent.lines.length >= numberOfLines + 5
|
|
||||||
) {
|
|
||||||
setExpandAllow(true)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ overflow: 'hidden' }}>
|
<View style={{ overflow: 'hidden' }}>
|
||||||
<CustomText
|
<CustomText
|
||||||
children={children}
|
children={children}
|
||||||
onTextLayout={onTextLayout}
|
onTextLayout={({ nativeEvent }) => {
|
||||||
|
if (
|
||||||
|
numberOfLines === 1 ||
|
||||||
|
nativeEvent.lines.length >= numberOfLines + 5
|
||||||
|
) {
|
||||||
|
setExpandAllow(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
numberOfLines={
|
numberOfLines={
|
||||||
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
|
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,17 +64,6 @@ const Timeline: React.FC<Props> = ({
|
||||||
? data.pages?.flatMap(page => [...page.body])
|
? 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(
|
const onEndReached = useCallback(
|
||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||||
[isFetchingNextPage]
|
[isFetchingNextPage]
|
||||||
|
@ -151,7 +140,17 @@ const Timeline: React.FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
|
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={
|
maintainVisibleContentPosition={
|
||||||
isFetching
|
isFetching
|
||||||
? {
|
? {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject, useCallback, useRef, useState } from 'react'
|
import React, { RefObject, useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { Circle } from 'react-native-animated-spinkit'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
|
@ -169,7 +169,7 @@ const TimelineRefresh: React.FC<Props> = ({
|
||||||
|
|
||||||
const arrowStage = useSharedValue(0)
|
const arrowStage = useSharedValue(0)
|
||||||
const onLayout = useCallback(
|
const onLayout = useCallback(
|
||||||
({ nativeEvent }) => {
|
({ nativeEvent }: LayoutChangeEvent) => {
|
||||||
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
||||||
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
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 { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
import { store } from '@root/store'
|
import { store } from '@root/store'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
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 i18next from 'i18next'
|
||||||
import { Alert, Linking, Platform } from 'react-native'
|
import { Asset, launchImageLibrary } from 'react-native-image-picker'
|
||||||
import ImagePicker, {
|
|
||||||
Image,
|
|
||||||
ImageOrVideo
|
|
||||||
} from 'react-native-image-crop-picker'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
mediaType?: 'photo' | 'video'
|
mediaType?: 'photo' | 'video'
|
||||||
|
@ -28,43 +21,7 @@ const mediaSelector = async ({
|
||||||
maximum,
|
maximum,
|
||||||
indicateMaximum = false,
|
indicateMaximum = false,
|
||||||
showActionSheetWithOptions
|
showActionSheetWithOptions
|
||||||
}: Props): Promise<({ uri: string } & Omit<ImageOrVideo, 'path'>)[]> => {
|
}: Props): Promise<Asset[]> => {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _maximum =
|
const _maximum =
|
||||||
maximum ||
|
maximum ||
|
||||||
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
|
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
|
||||||
|
@ -105,79 +62,30 @@ const mediaSelector = async ({
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const selectImage = async () => {
|
const selectImage = async () => {
|
||||||
const images = await ImagePicker.openPicker({
|
const images = await launchImageLibrary({
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
includeExif: false,
|
...(resize && { maxWidth: resize.width, maxHeight: resize.height }),
|
||||||
multiple: true,
|
includeBase64: false,
|
||||||
minFiles: 1,
|
includeExtra: false,
|
||||||
maxFiles: _maximum,
|
selectionLimit: _maximum
|
||||||
smartAlbums: ['UserLibrary'],
|
})
|
||||||
writeTempFile: false,
|
|
||||||
loadingLabelText: ''
|
|
||||||
}).catch(() => {})
|
|
||||||
|
|
||||||
if (!images) {
|
if (!images.assets) {
|
||||||
return reject()
|
return reject()
|
||||||
}
|
}
|
||||||
|
|
||||||
// react-native-image-crop-picker may return HEIC as JPG that causes upload failure
|
return resolve(images.assets)
|
||||||
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}`
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const selectVideo = async () => {
|
const selectVideo = async () => {
|
||||||
const video = await ImagePicker.openPicker({
|
const video = await launchImageLibrary({
|
||||||
mediaType: 'video',
|
mediaType: 'video',
|
||||||
includeExif: false,
|
includeBase64: false,
|
||||||
loadingLabelText: ''
|
includeExtra: false,
|
||||||
}).catch(() => {})
|
selectionLimit: 1
|
||||||
|
})
|
||||||
|
|
||||||
if (video) {
|
if (video.assets?.[0]) {
|
||||||
return resolve([
|
return resolve(video.assets)
|
||||||
{ ...video, uri: video.sourceURL || `file://${video.path}` }
|
|
||||||
])
|
|
||||||
} else {
|
} else {
|
||||||
return reject()
|
return reject()
|
||||||
}
|
}
|
||||||
|
@ -189,10 +97,6 @@ const mediaSelector = async ({
|
||||||
cancelButtonIndex: mediaType ? 1 : 2
|
cancelButtonIndex: mediaType ? 1 : 2
|
||||||
},
|
},
|
||||||
async buttonIndex => {
|
async buttonIndex => {
|
||||||
if (!(await checkLibraryPermission())) {
|
|
||||||
return reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
case 'photo':
|
case 'photo':
|
||||||
if (buttonIndex === 0) {
|
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { FormattedRelativeTime } from 'react-intl'
|
import {
|
||||||
import { Dimensions, Platform, Pressable, StyleSheet, View } from 'react-native'
|
Dimensions,
|
||||||
|
NativeScrollEvent,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
Platform,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
||||||
|
@ -92,9 +99,7 @@ const ScreenAnnouncements: React.FC<
|
||||||
>
|
>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='screenAnnouncements:content.published'
|
i18nKey='screenAnnouncements:content.published'
|
||||||
components={[
|
components={[<RelativeTime time={item.published_at} />]}
|
||||||
<RelativeTime time={item.published_at} />
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</CustomText>
|
</CustomText>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
@ -218,7 +223,7 @@ const ScreenAnnouncements: React.FC<
|
||||||
contentOffset: { x },
|
contentOffset: { x },
|
||||||
layoutMeasurement: { width }
|
layoutMeasurement: { width }
|
||||||
}
|
}
|
||||||
}) => {
|
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
setIndex(Math.floor(x / width))
|
setIndex(Math.floor(x / width))
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -150,7 +150,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||||
for (const m of params.media) {
|
for (const m of params.media) {
|
||||||
uploadAttachment({
|
uploadAttachment({
|
||||||
composeDispatch,
|
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 [checkingAttachments, setCheckingAttachments] = useState(false)
|
||||||
|
|
||||||
const removeDraft = useCallback(ts => {
|
|
||||||
dispatch(removeInstanceDraft(ts))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: ComposeStateDraft }) => {
|
({ item }: { item: ComposeStateDraft }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -144,7 +140,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||||
}}
|
}}
|
||||||
source={{
|
source={{
|
||||||
uri:
|
uri:
|
||||||
attachment.local?.local_thumbnail ||
|
attachment.local?.thumbnail ||
|
||||||
attachment.remote?.preview_url
|
attachment.remote?.preview_url
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -157,38 +153,6 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||||
},
|
},
|
||||||
[theme]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -220,7 +184,35 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||||
<SwipeListView
|
<SwipeListView
|
||||||
data={instanceDrafts}
|
data={instanceDrafts}
|
||||||
renderItem={renderItem}
|
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}
|
disableRightSwipe={true}
|
||||||
rightOpenValue={-actionWidth}
|
rightOpenValue={-actionWidth}
|
||||||
// previewRowKey={
|
// previewRowKey={
|
||||||
|
|
|
@ -35,7 +35,7 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
|
||||||
video.local
|
video.local
|
||||||
? ({
|
? ({
|
||||||
url: video.local.uri,
|
url: video.local.uri,
|
||||||
preview_url: video.local.local_thumbnail,
|
preview_url: video.local.thumbnail,
|
||||||
blurhash: video.remote?.blurhash
|
blurhash: video.remote?.blurhash
|
||||||
} as Mastodon.AttachmentVideo)
|
} as Mastodon.AttachmentVideo)
|
||||||
: (video.remote 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 { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, {
|
import React, { useContext, useEffect, useMemo, useRef } from 'react'
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef
|
|
||||||
} from 'react'
|
|
||||||
import {
|
import {
|
||||||
AccessibilityInfo,
|
AccessibilityInfo,
|
||||||
findNodeHandle,
|
findNodeHandle,
|
||||||
|
@ -147,35 +141,25 @@ const ComposeRoot = React.memo(
|
||||||
}
|
}
|
||||||
}, [isFetching])
|
}, [isFetching])
|
||||||
|
|
||||||
const listItem = useCallback(
|
|
||||||
({ item }) => (
|
|
||||||
<ComposeRootSuggestion
|
|
||||||
item={item}
|
|
||||||
composeState={composeState}
|
|
||||||
composeDispatch={composeDispatch}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[composeState]
|
|
||||||
)
|
|
||||||
|
|
||||||
const ListFooter = useCallback(
|
|
||||||
() => (
|
|
||||||
<ComposeRootFooter
|
|
||||||
accessibleRefAttachments={accessibleRefAttachments}
|
|
||||||
accessibleRefEmojis={accessibleRefEmojis}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<FlatList
|
<FlatList
|
||||||
renderItem={listItem}
|
renderItem={({ item }) => (
|
||||||
|
<ComposeRootSuggestion
|
||||||
|
item={item}
|
||||||
|
composeState={composeState}
|
||||||
|
composeDispatch={composeDispatch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
ListHeaderComponent={ComposeRootHeader}
|
ListHeaderComponent={ComposeRootHeader}
|
||||||
ListFooterComponent={ListFooter}
|
ListFooterComponent={() => (
|
||||||
|
<ComposeRootFooter
|
||||||
|
accessibleRefAttachments={accessibleRefAttachments}
|
||||||
|
accessibleRefEmojis={accessibleRefEmojis}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data={data ? data[composeState.tag?.type] : undefined}
|
data={data ? data[composeState.tag?.type] : undefined}
|
||||||
|
|
|
@ -56,9 +56,12 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||||
})
|
})
|
||||||
}, [composeState.attachments.sensitive])
|
}, [composeState.attachments.sensitive])
|
||||||
|
|
||||||
const calculateWidth = useCallback(item => {
|
const calculateWidth = useCallback((item: ExtendedAttachment) => {
|
||||||
if (item.local) {
|
if (item.local) {
|
||||||
return (item.local.width / item.local.height) * DEFAULT_HEIGHT
|
return (
|
||||||
|
((item.local.width || 100) / (item.local.height || 100)) *
|
||||||
|
DEFAULT_HEIGHT
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (item.remote) {
|
if (item.remote) {
|
||||||
if (item.remote.meta.original.aspect) {
|
if (item.remote.meta.original.aspect) {
|
||||||
|
@ -135,7 +138,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||||
<FastImage
|
<FastImage
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
source={{
|
source={{
|
||||||
uri: item.local?.local_thumbnail || item.remote?.preview_url
|
uri: item.local?.thumbnail || item.remote?.preview_url
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{item.remote?.meta?.original?.duration ? (
|
{item.remote?.meta?.original?.duration ? (
|
||||||
|
|
|
@ -42,22 +42,6 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||||
}
|
}
|
||||||
}, [composeState.emoji.active])
|
}, [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(
|
const listItem = useCallback(
|
||||||
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -155,7 +139,18 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
sections={composeState.emoji.emojis || []}
|
sections={composeState.emoji.emojis || []}
|
||||||
keyExtractor={item => item[0].shortcode}
|
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}
|
renderItem={listItem}
|
||||||
windowSize={2}
|
windowSize={2}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import apiInstance from '@api/instance'
|
import apiInstance from '@api/instance'
|
||||||
import mediaSelector from '@components/mediaSelector'
|
import mediaSelector from '@components/mediaSelector'
|
||||||
import { ImageOrVideo } from 'react-native-image-crop-picker'
|
import { Asset } from 'react-native-image-picker'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
|
@ -22,46 +22,40 @@ export const uploadAttachment = async ({
|
||||||
media
|
media
|
||||||
}: {
|
}: {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
media: { uri: string } & Pick<ImageOrVideo, 'mime' | 'width' | 'height'>
|
media: Required<Pick<Asset, 'uri' | 'type' | 'fileName'>>
|
||||||
}) => {
|
}) => {
|
||||||
const hash = await Crypto.digestStringAsync(
|
const hash = await Crypto.digestStringAsync(
|
||||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||||
media.uri + Math.random()
|
media.uri + Math.random()
|
||||||
)
|
)
|
||||||
|
|
||||||
switch (media.mime.split('/')[0]) {
|
switch (media.type.split('/')[0]) {
|
||||||
case 'image':
|
case 'image':
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...media, type: 'image', local_thumbnail: media.uri, hash },
|
local: { ...media, thumbnail: media.uri, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'video':
|
case 'video':
|
||||||
VideoThumbnails.getThumbnailAsync(media.uri)
|
VideoThumbnails.getThumbnailAsync(media.uri)
|
||||||
.then(({ uri, width, height }) =>
|
.then(({ uri, width, height }) => {
|
||||||
|
console.log('new', uri, width, height)
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: {
|
local: { ...media, thumbnail: uri, hash, width, height },
|
||||||
...media,
|
|
||||||
type: 'video',
|
|
||||||
local_thumbnail: uri,
|
|
||||||
hash,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
},
|
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...media, type: 'video', hash },
|
local: { ...media, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -71,7 +65,7 @@ export const uploadAttachment = async ({
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...media, type: 'unknown', hash },
|
local: { ...media, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -102,8 +96,8 @@ export const uploadAttachment = async ({
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', {
|
formData.append('file', {
|
||||||
uri: media.uri,
|
uri: media.uri,
|
||||||
name: media.uri.match(new RegExp(/.*\/(.*)/))?.[1] || 'file.jpg',
|
name: media.fileName,
|
||||||
type: media.mime
|
type: media.type
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
return apiInstance<Mastodon.Attachment>({
|
return apiInstance<Mastodon.Attachment>({
|
||||||
|
@ -140,7 +134,8 @@ const chooseAndUploadAttachment = async ({
|
||||||
showActionSheetWithOptions
|
showActionSheetWithOptions
|
||||||
})
|
})
|
||||||
for (const media of result) {
|
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))
|
await new Promise(res => setTimeout(res, 500))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,15 +101,7 @@ const ComposeTextInput: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
uploadAttachment({
|
uploadAttachment({ composeDispatch, media: file })
|
||||||
composeDispatch,
|
|
||||||
media: {
|
|
||||||
uri: file.uri,
|
|
||||||
mime: file.type,
|
|
||||||
width: 100,
|
|
||||||
height: 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { ImageOrVideo } from 'react-native-image-crop-picker'
|
import { Asset } from 'react-native-image-picker'
|
||||||
|
|
||||||
export type ExtendedAttachment = {
|
export type ExtendedAttachment = {
|
||||||
remote?: Mastodon.Attachment
|
remote?: Mastodon.Attachment
|
||||||
local?: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'> & {
|
local?: Asset & { thumbnail?: string; hash: string }
|
||||||
type: 'image' | 'video' | 'unknown'
|
|
||||||
local_thumbnail?: string
|
|
||||||
hash?: string
|
|
||||||
}
|
|
||||||
uploading?: boolean
|
uploading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,10 +117,7 @@ export type ComposeAction =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/upload/end'
|
type: 'attachment/upload/end'
|
||||||
payload: {
|
payload: { remote: Mastodon.Attachment; local: Asset }
|
||||||
remote: Mastodon.Attachment
|
|
||||||
local: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachment/upload/fail'
|
type: 'attachment/upload/fail'
|
||||||
|
|
|
@ -50,11 +50,9 @@ const usePanResponder = ({
|
||||||
onLongPress,
|
onLongPress,
|
||||||
delayLongPress,
|
delayLongPress,
|
||||||
onRequestClose
|
onRequestClose
|
||||||
}: Props): Readonly<[
|
}: Props): Readonly<
|
||||||
GestureResponderHandlers,
|
[GestureResponderHandlers, Animated.Value, Animated.ValueXY]
|
||||||
Animated.Value,
|
> => {
|
||||||
Animated.ValueXY
|
|
||||||
]> => {
|
|
||||||
let numberInitialTouches = 1
|
let numberInitialTouches = 1
|
||||||
let initialTouches: NativeTouchEvent[] = []
|
let initialTouches: NativeTouchEvent[] = []
|
||||||
let currentScale = initialScale
|
let currentScale = initialScale
|
||||||
|
@ -137,6 +135,7 @@ const usePanResponder = ({
|
||||||
|
|
||||||
if (gestureState.numberActiveTouches > 1) return
|
if (gestureState.numberActiveTouches > 1) return
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
|
longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
|
||||||
},
|
},
|
||||||
onStart: (
|
onStart: (
|
||||||
|
@ -150,6 +149,7 @@ const usePanResponder = ({
|
||||||
|
|
||||||
const tapTS = Date.now()
|
const tapTS = Date.now()
|
||||||
!timer &&
|
!timer &&
|
||||||
|
// @ts-ignore
|
||||||
(timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
|
(timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
|
||||||
// Handle double tap event by calculating diff between first and second taps timestamps
|
// Handle double tap event by calculating diff between first and second taps timestamps
|
||||||
|
|
||||||
|
@ -158,6 +158,7 @@ const usePanResponder = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||||
|
// @ts-ignore
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
|
const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
|
||||||
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
|
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
|
||||||
|
@ -291,9 +292,8 @@ const usePanResponder = ({
|
||||||
if (isTapGesture && currentScale > initialScale) {
|
if (isTapGesture && currentScale > initialScale) {
|
||||||
const { x, y } = currentTranslate
|
const { x, y } = currentTranslate
|
||||||
const { dx, dy } = gestureState
|
const { dx, dy } = gestureState
|
||||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
const [topBound, leftBound, bottomBound, rightBound] =
|
||||||
currentScale
|
getBounds(currentScale)
|
||||||
)
|
|
||||||
|
|
||||||
let nextTranslateX = x + dx
|
let nextTranslateX = x + dx
|
||||||
let nextTranslateY = y + dy
|
let nextTranslateY = y + dy
|
||||||
|
@ -357,9 +357,8 @@ const usePanResponder = ({
|
||||||
|
|
||||||
if (tmpTranslate) {
|
if (tmpTranslate) {
|
||||||
const { x, y } = tmpTranslate
|
const { x, y } = tmpTranslate
|
||||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
const [topBound, leftBound, bottomBound, rightBound] =
|
||||||
currentScale
|
getBounds(currentScale)
|
||||||
)
|
|
||||||
|
|
||||||
let nextTranslateX = x
|
let nextTranslateX = x
|
||||||
let nextTranslateY = y
|
let nextTranslateY = y
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const getImageStyles = (
|
||||||
const transform = translate.getTranslateTransform()
|
const transform = translate.getTranslateTransform()
|
||||||
|
|
||||||
if (scale) {
|
if (scale) {
|
||||||
|
// @ts-ignore
|
||||||
transform.push({ scale }, { perspective: new Animated.Value(1000) })
|
transform.push({ scale }, { perspective: new Animated.Value(1000) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import {
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
BottomTabNavigationOptions,
|
|
||||||
createBottomTabNavigator
|
|
||||||
} from '@react-navigation/bottom-tabs'
|
|
||||||
import { useAppDispatch } from '@root/store'
|
import { useAppDispatch } from '@root/store'
|
||||||
import {
|
import {
|
||||||
RootStackScreenProps,
|
RootStackScreenProps,
|
||||||
|
@ -15,10 +12,7 @@ import {
|
||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
getInstanceActive
|
getInstanceActive
|
||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
import {
|
import { getVersionUpdate, retriveVersionLatest } from '@utils/slices/appSlice'
|
||||||
getVersionUpdate,
|
|
||||||
retriveVersionLatest
|
|
||||||
} from '@utils/slices/appSlice'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
|
@ -32,7 +26,7 @@ const Tab = createBottomTabNavigator<ScreenTabsStackParamList>()
|
||||||
|
|
||||||
const ScreenTabs = React.memo(
|
const ScreenTabs = React.memo(
|
||||||
({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
|
({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
const instanceActive = useSelector(getInstanceActive)
|
||||||
const instanceAccount = useSelector(
|
const instanceAccount = useSelector(
|
||||||
|
@ -40,57 +34,6 @@ const ScreenTabs = React.memo(
|
||||||
(prev, next) => prev?.avatarStatic === next?.avatarStatic
|
(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(
|
const composeListeners = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tabPress: (e: any) => {
|
tabPress: (e: any) => {
|
||||||
|
@ -132,7 +75,53 @@ const ScreenTabs = React.memo(
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
|
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-Local' component={TabLocal} />
|
||||||
<Tab.Screen name='Tab-Public' component={TabPublic} />
|
<Tab.Screen name='Tab-Public' component={TabPublic} />
|
||||||
|
|
|
@ -44,16 +44,16 @@ const TabLocal = React.memo(
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const children = useCallback(
|
const children = useCallback(
|
||||||
() => (
|
() => (
|
||||||
<Timeline
|
<Timeline
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
lookback='Following'
|
lookback='Following'
|
||||||
customProps={{ renderItem }}
|
customProps={{
|
||||||
|
renderItem: ({ item }) => (
|
||||||
|
<TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabMeBookmarks = React.memo(
|
const TabMeBookmarks = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
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
|
() => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
import TimelineConversation from '@components/Timeline/Conversation'
|
import TimelineConversation from '@components/Timeline/Conversation'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabMeConversations = React.memo(
|
const TabMeConversations = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
|
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
|
() => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabMeFavourites = React.memo(
|
const TabMeFavourites = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
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
|
() => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Timeline from '@components/Timeline'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
|
const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
|
||||||
route: {
|
route: {
|
||||||
|
@ -10,12 +10,17 @@ const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', 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
|
export default TabMeListsList
|
||||||
|
|
|
@ -43,14 +43,17 @@ const TabNotifications = React.memo(
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item }) => (
|
|
||||||
<TimelineNotifications notification={item} queryKey={queryKey} />
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const children = useCallback(
|
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 scrollY = useSharedValue(0)
|
||||||
|
|
||||||
const onScroll = useCallback(({ nativeEvent }) => {
|
|
||||||
scrollY.value = nativeEvent.contentOffset.y
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Account_Default', account: account.id }
|
{ page: 'Account_Default', account: account.id }
|
||||||
])
|
])
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const isFetchingTimeline = useIsFetching(queryKey)
|
const isFetchingTimeline = useIsFetching(queryKey)
|
||||||
const fetchedTimeline = useRef(false)
|
const fetchedTimeline = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -97,8 +89,11 @@ const TabSharedAccount: React.FC<
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
disableRefresh
|
disableRefresh
|
||||||
customProps={{
|
customProps={{
|
||||||
renderItem,
|
renderItem: ({ item }) => (
|
||||||
onScroll,
|
<TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
),
|
||||||
|
onScroll: ({ nativeEvent }) =>
|
||||||
|
(scrollY.value = nativeEvent.contentOffset.y),
|
||||||
ListHeaderComponent,
|
ListHeaderComponent,
|
||||||
maintainVisibleContentPosition: undefined
|
maintainVisibleContentPosition: undefined
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useRoute } from '@react-navigation/native'
|
import { useRoute } from '@react-navigation/native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { Placeholder, Fade } from 'rn-placeholder'
|
import { Placeholder, Fade } from 'rn-placeholder'
|
||||||
import AccountInformationAccount from './Information/Account'
|
import AccountInformationAccount from './Information/Account'
|
||||||
|
@ -19,21 +19,21 @@ export interface Props {
|
||||||
|
|
||||||
const AccountInformation = React.memo(
|
const AccountInformation = React.memo(
|
||||||
({ account }: Props) => {
|
({ account }: Props) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const { name } = useRoute()
|
const { name } = useRoute()
|
||||||
const myInfo = name !== 'Tab-Shared-Account'
|
const myInfo = name !== 'Tab-Shared-Account'
|
||||||
|
|
||||||
const animation = useCallback(
|
|
||||||
props => (
|
|
||||||
<Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} />
|
|
||||||
),
|
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<Placeholder Animation={animation}>
|
<Placeholder
|
||||||
|
Animation={props => (
|
||||||
|
<Fade
|
||||||
|
{...props}
|
||||||
|
style={{ backgroundColor: colors.shimmerHighlight }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
<View style={styles.avatarAndActions}>
|
<View style={styles.avatarAndActions}>
|
||||||
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
||||||
<AccountInformationActions 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 TimelineDefault from '@components/Timeline/Default'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabSharedAttachments: React.FC<TabSharedStackScreenProps<
|
const TabSharedAttachments: React.FC<
|
||||||
'Tab-Shared-Attachments'
|
TabSharedStackScreenProps<'Tab-Shared-Attachments'>
|
||||||
>> = ({
|
> = ({
|
||||||
route: {
|
route: {
|
||||||
params: { account }
|
params: { account }
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,16 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Account_Attachments', account: account.id }
|
{ page: 'Account_Attachments', account: account.id }
|
||||||
]
|
]
|
||||||
const renderItem = useCallback(
|
return (
|
||||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
<Timeline
|
||||||
[]
|
queryKey={queryKey}
|
||||||
|
customProps={{
|
||||||
|
renderItem: ({ item }) => (
|
||||||
|
<TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TabSharedAttachments
|
export default TabSharedAttachments
|
||||||
|
|
|
@ -2,21 +2,26 @@ import Timeline from '@components/Timeline'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<
|
const TabSharedHashtag: React.FC<
|
||||||
'Tab-Shared-Hashtag'
|
TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
|
||||||
>> = ({
|
> = ({
|
||||||
route: {
|
route: {
|
||||||
params: { hashtag }
|
params: { hashtag }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
|
||||||
const renderItem = useCallback(
|
return (
|
||||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
<Timeline
|
||||||
[]
|
queryKey={queryKey}
|
||||||
|
customProps={{
|
||||||
|
renderItem: ({ item }) => (
|
||||||
|
<TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TabSharedHashtag
|
export default TabSharedHashtag
|
||||||
|
|
|
@ -139,65 +139,22 @@ const TabSharedSearch: React.FC<
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [status])
|
}, [status])
|
||||||
const sectionHeader = useCallback(
|
|
||||||
({ section: { translation } }) => (
|
const listItem = useCallback(
|
||||||
<View
|
({ item, section }: { item: any; section: any }) => {
|
||||||
style={{
|
switch (section.title) {
|
||||||
padding: StyleConstants.Spacing.M,
|
case 'accounts':
|
||||||
backgroundColor: colors.backgroundDefault
|
return <ComponentAccount account={item} origin='search' />
|
||||||
}}
|
case 'hashtags':
|
||||||
>
|
return <ComponentHashtag hashtag={item} origin='search' />
|
||||||
<CustomText
|
case 'statuses':
|
||||||
fontStyle='M'
|
return <TimelineDefault item={item} disableDetails origin='search' />
|
||||||
style={{
|
default:
|
||||||
textAlign: 'center',
|
return null
|
||||||
color: colors.primaryDefault
|
}
|
||||||
}}
|
},
|
||||||
fontWeight='Bold'
|
|
||||||
>
|
|
||||||
{translation}
|
|
||||||
</CustomText>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
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 (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
|
@ -211,8 +168,48 @@ const TabSharedSearch: React.FC<
|
||||||
sections={data || []}
|
sections={data || []}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
renderSectionHeader={sectionHeader}
|
renderSectionHeader={({ section: { translation } }) => (
|
||||||
renderSectionFooter={sectionFooter}
|
<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}
|
keyExtractor={(item, index) => item + index}
|
||||||
SectionSeparatorComponent={ComponentSeparator}
|
SectionSeparatorComponent={ComponentSeparator}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
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 { FlatList } from 'react-native'
|
||||||
import { InfiniteQueryObserver, useQueryClient } from 'react-query'
|
import { InfiniteQueryObserver, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
|
@ -59,43 +59,35 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||||
})
|
})
|
||||||
}, [scrolled.current])
|
}, [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 (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
flRef={flRef}
|
flRef={flRef}
|
||||||
queryKey={queryKey}
|
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
|
disableRefresh
|
||||||
disableInfinity
|
disableInfinity
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,28 +9,20 @@ import { FlatList } from 'react-native-gesture-handler'
|
||||||
const TabSharedUsers = React.memo(
|
const TabSharedUsers = React.memo(
|
||||||
({ route: { params } }: TabSharedStackScreenProps<'Tab-Shared-Users'>) => {
|
({ route: { params } }: TabSharedStackScreenProps<'Tab-Shared-Users'>) => {
|
||||||
const queryKey: QueryKeyUsers = ['Users', params]
|
const queryKey: QueryKeyUsers = ['Users', params]
|
||||||
const {
|
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||||
data,
|
useUsersQuery({
|
||||||
hasNextPage,
|
...queryKey[1],
|
||||||
fetchNextPage,
|
options: {
|
||||||
isFetchingNextPage
|
getPreviousPageParam: firstPage =>
|
||||||
} = useUsersQuery({
|
firstPage.links?.prev && { since_id: firstPage.links.next },
|
||||||
...queryKey[1],
|
getNextPageParam: lastPage =>
|
||||||
options: {
|
lastPage.links?.next && { max_id: lastPage.links.next }
|
||||||
getPreviousPageParam: firstPage =>
|
}
|
||||||
firstPage.links?.prev && { since_id: firstPage.links.next },
|
})
|
||||||
getNextPageParam: lastPage =>
|
|
||||||
lastPage.links?.next && { max_id: lastPage.links.next }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const flattenData = data?.pages
|
const flattenData = data?.pages
|
||||||
? data.pages.flatMap(page => [...page.body])
|
? data.pages.flatMap(page => [...page.body])
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
({ item }) => <ComponentAccount account={item} origin='relationship' />,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const onEndReached = useCallback(
|
const onEndReached = useCallback(
|
||||||
() => hasNextPage && !isFetchingNextPage && fetchNextPage(),
|
() => hasNextPage && !isFetchingNextPage && fetchNextPage(),
|
||||||
[hasNextPage, isFetchingNextPage]
|
[hasNextPage, isFetchingNextPage]
|
||||||
|
@ -41,7 +33,9 @@ const TabSharedUsers = React.memo(
|
||||||
windowSize={7}
|
windowSize={7}
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
style={styles.flatList}
|
style={styles.flatList}
|
||||||
renderItem={renderItem}
|
renderItem={({ item }) => (
|
||||||
|
<ComponentAccount account={item} origin='relationship' />
|
||||||
|
)}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
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'
|
import { AccessibilityInfo } from 'react-native'
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
|
@ -15,7 +21,7 @@ const AccessibilityContext = createContext<ContextType>({
|
||||||
|
|
||||||
export const useAccessibility = () => useContext(AccessibilityContext)
|
export const useAccessibility = () => useContext(AccessibilityContext)
|
||||||
|
|
||||||
const AccessibilityManager: React.FC = ({ children }) => {
|
const AccessibilityManager: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false)
|
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false)
|
||||||
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false)
|
const [screenReaderEnabled, setScreenReaderEnabled] = useState(false)
|
||||||
const [boldTextEnabled, setBoldTextEnabled] = 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 { Appearance } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { ColorDefinitions, getColors, Theme } from '@utils/styles/themes'
|
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 osTheme = useColorSchemeDelay()
|
||||||
const userTheme = useSelector(getSettingsTheme)
|
const userTheme = useSelector(getSettingsTheme)
|
||||||
const darkTheme = useSelector(getSettingsDarkTheme)
|
const darkTheme = useSelector(getSettingsDarkTheme)
|
||||||
|
|
Loading…
Reference in New Issue