This commit is contained in:
xmflsct 2022-08-07 01:18:10 +02:00
parent 99b38f421c
commit e2ba4660df
40 changed files with 462 additions and 752 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"javascript.inlayHints.functionLikeReturnTypes.enabled": false
}

View File

@ -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>

View File

@ -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 })
}
},

View File

@ -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,

View File

@ -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}
/>

View File

@ -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,

View File

@ -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'

View File

@ -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
}

View File

@ -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
? {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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

View File

@ -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))
},
[]

View File

@ -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 }
})
}
}

View File

@ -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={

View File

@ -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)

View File

@ -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}

View File

@ -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 ? (

View File

@ -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}
/>

View File

@ -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))
}
}

View File

@ -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 })
}
}}
>

View File

@ -1,12 +1,8 @@
import { ImageOrVideo } from 'react-native-image-crop-picker'
import { Asset } from 'react-native-image-picker'
export type ExtendedAttachment = {
remote?: Mastodon.Attachment
local?: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'> & {
type: 'image' | 'video' | 'unknown'
local_thumbnail?: string
hash?: string
}
local?: Asset & { thumbnail?: string; hash: string }
uploading?: boolean
}
@ -121,10 +117,7 @@ export type ComposeAction =
}
| {
type: 'attachment/upload/end'
payload: {
remote: Mastodon.Attachment
local: { uri: string } & Pick<ImageOrVideo, 'width' | 'height' | 'mime'>
}
payload: { remote: Mastodon.Attachment; local: Asset }
}
| {
type: 'attachment/upload/fail'

View File

@ -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

View File

@ -44,6 +44,7 @@ export const getImageStyles = (
const transform = translate.getTranslateTransform()
if (scale) {
// @ts-ignore
transform.push({ scale }, { perspective: new Animated.Value(1000) })
}

View File

@ -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} />

View File

@ -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} />
)
}}
/>
),
[]

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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

View File

@ -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} />
)
}}
/>
),
[]
)

View File

@ -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
}}

View File

@ -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} />

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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
/>

View File

@ -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}

View File

@ -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)

View File

@ -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)