Merge branch 'main' into candidate

This commit is contained in:
xmflsct 2023-01-08 00:59:11 +01:00
commit a60fb546d8
33 changed files with 411 additions and 346 deletions

View File

@ -231,6 +231,7 @@ android {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
namespace 'com.xmflsct.app.tooot'
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="com.xmflsct.app.tooot">
xmlns:tools="http://schemas.android.com/tools"
package="com.xmflsct.app.tooot">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

View File

@ -22,7 +22,7 @@ buildscript {
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:7.2.1")
classpath('com.android.tools.build:gradle:7.3.1')
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:5.0.1")

View File

@ -26,7 +26,7 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.125.0
FLIPPER_VERSION=0.176.1
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using

View File

@ -1,5 +1,6 @@
Enjoy toooting! This version includes following improvements and fixes:
- Auto fetch remote content in conversations!
- Remember last read position in timeline!
- Allowing adding more context of reports
- Option to disable autoplay gif
- Hide boosts from users

View File

@ -1,5 +1,6 @@
toooting愉快此版本包括以下改进和修复
- 主动获取对话的远程内容
- 自动加载上次我的关注的阅读位置
- 可添加举报细节
- 新增暂停自动播放gif动画选项
- 隐藏用户的转嘟

View File

@ -16,7 +16,7 @@ PODS:
- ExpoModulesCore
- EXNotifications (0.17.0):
- ExpoModulesCore
- Expo (47.0.11):
- Expo (47.0.12):
- ExpoModulesCore
- ExpoCrypto (12.1.0):
- ExpoModulesCore
@ -26,7 +26,7 @@ PODS:
- ExpoModulesCore
- ExpoLocalization (14.0.0):
- ExpoModulesCore
- ExpoModulesCore (1.1.0):
- ExpoModulesCore (1.1.1):
- React-Core
- ReactCommon/turbomodule/core
- ExpoRandom (13.0.0):
@ -39,6 +39,9 @@ PODS:
- ExpoModulesCore
- EXScreenCapture (5.0.0):
- ExpoModulesCore
- EXScreenOrientation (5.0.1):
- ExpoModulesCore
- React-Core
- EXSecureStore (12.0.0):
- ExpoModulesCore
- EXSplashScreen (0.17.5):
@ -298,7 +301,7 @@ PODS:
- React-Core
- react-native-blurhash (1.1.10):
- React-Core
- react-native-cameraroll (5.2.0):
- react-native-cameraroll (5.2.1):
- React-Core
- react-native-image-picker (4.10.3):
- React-Core
@ -470,6 +473,7 @@ DEPENDENCIES:
- ExpoVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
- EXScreenCapture (from `../node_modules/expo-screen-capture/ios`)
- EXScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
- EXSecureStore (from `../node_modules/expo-secure-store/ios`)
- EXSplashScreen (from `../node_modules/expo-splash-screen/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
@ -582,6 +586,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-web-browser/ios"
EXScreenCapture:
:path: "../node_modules/expo-screen-capture/ios"
EXScreenOrientation:
:path: "../node_modules/expo-screen-orientation/ios"
EXSecureStore:
:path: "../node_modules/expo-secure-store/ios"
EXSplashScreen:
@ -705,17 +711,18 @@ SPEC CHECKSUMS:
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
Expo: dedd83acfd4d70cbec6ac2f1b4433462d95c70bc
Expo: f48d305fda3e4e501d686e6bad7d8c8373828279
ExpoCrypto: 6eb2a5ede7d95b7359a5f0391ee0c5d2ecd144b3
ExpoHaptics: 129d3f8d44c2205adcdf8db760602818463d5437
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
ExpoLocalization: e202d1e2a4950df17ac8d0889d65a1ffd7532d7e
ExpoModulesCore: 089e1ac0f0edee4dd0af0eb4e3f7b44d72cc418d
ExpoModulesCore: 485dff3a59b036a33b6050c0a5aea3cf1037fdd1
ExpoRandom: 58b7e0a5fe1adf1cb6dc1cbe503a6fe9524f36ce
ExpoStoreReview: 713336ff504db3a6983475bf7c67519cc5efc86f
ExpoVideoThumbnails: 424db02cedfbbe2d498bcb2712ea4ba8a9dcb453
ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda
EXScreenCapture: d9f1ec31042dfef109290d06c2b4789b7444d16d
EXScreenOrientation: 07e5aeff07bce09a2b214981e612d87fd7719997
EXSecureStore: daec0117c922a67c658cb229152a9e252e5c1750
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4
@ -744,7 +751,7 @@ SPEC CHECKSUMS:
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
react-native-cameraroll: f94bf9f46c998963ecd2bb6e9a3f9cca59b6d9f1
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0

View File

@ -27,7 +27,7 @@
"@mattermost/react-native-paste-input": "^0.5.2",
"@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
"@react-native-async-storage/async-storage": "~1.17.11",
"@react-native-camera-roll/camera-roll": "^5.2.0",
"@react-native-camera-roll/camera-roll": "^5.2.1",
"@react-native-clipboard/clipboard": "^1.11.1",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/netinfo": "9.3.7",
@ -42,7 +42,7 @@
"@tanstack/react-query": "^4.20.9",
"axios": "^1.2.2",
"diff": "^5.1.0",
"expo": "^47.0.11",
"expo": "^47.0.12",
"expo-auth-session": "^3.8.0",
"expo-av": "^13.1.0",
"expo-constants": "^14.1.0",
@ -54,6 +54,7 @@
"expo-notifications": "^0.17.0",
"expo-random": "^13.0.0",
"expo-screen-capture": "^5.0.0",
"expo-screen-orientation": "^5.0.1",
"expo-secure-store": "^12.0.0",
"expo-splash-screen": "^0.17.5",
"expo-store-review": "^6.1.0",
@ -65,7 +66,7 @@
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.1.1",
"react-i18next": "^12.1.4",
"react-intl": "^6.2.5",
"react-native": "^0.70.6",
"react-native-animated-spinkit": "^1.5.2",

View File

@ -49,10 +49,10 @@ const ParseHTML: React.FC<Props> = ({
StyleConstants.Font.Size[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight = adaptiveScale(
StyleConstants.Font.LineHeight[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight =
Platform.OS === 'ios'
? adaptiveScale(StyleConstants.Font.LineHeight[size], adaptiveSize ? adaptiveFontsize : 0)
: undefined
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { params } = useRoute()
@ -119,7 +119,11 @@ const ParseHTML: React.FC<Props> = ({
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1].toLowerCase()
const children = node.children.map(unwrapNode).join('')
const tag =
href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1]?.toLowerCase() ||
children.match(new RegExp(/#(\S+)/))?.[1]?.toLowerCase()
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
?.hashtag
const sameHashtag = paramsHashtag === tag
@ -143,7 +147,7 @@ const ParseHTML: React.FC<Props> = ({
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
}
children={node.children.map(unwrapNode).join('')}
children={children}
/>
)
}
@ -199,7 +203,10 @@ const ParseHTML: React.FC<Props> = ({
break
case 'br':
return (
<Text key={index} style={{ lineHeight: adaptedLineheight / 2 }}>
<Text
key={index}
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
>
{'\n'}
</Text>
)
@ -208,7 +215,11 @@ const ParseHTML: React.FC<Props> = ({
return (
<Text key={index}>
{node.children.map((c, i) => renderNode(c, i))}
<Text style={{ lineHeight: adaptedLineheight / 2 }}>{'\n\n'}</Text>
<Text
style={{ lineHeight: adaptedLineheight ? adaptedLineheight / 2 : undefined }}
>
{'\n\n'}
</Text>
</Text>
)
} else {

View File

@ -16,16 +16,7 @@ export interface Props {
const TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
const { hasNextPage } = useTimelineQuery({
...queryKey[1],
options: {
enabled: !disableInfinity,
notifyOnChangeProps: ['hasNextPage'],
getNextPageParam: lastPage =>
lastPage?.links?.next && {
...(lastPage.links.next.isOffset
? { offset: lastPage.links.next.id }
: { max_id: lastPage.links.next.id })
}
}
options: { enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'] }
})
const { colors } = useTheme()

View File

@ -7,18 +7,19 @@ import {
QueryKeyTimeline,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { setAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useEffect, useRef, useState } from 'react'
import React, { RefObject, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming
} from 'react-native-reanimated'
@ -26,21 +27,25 @@ import Animated, {
export interface Props {
flRef: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
fetchingActive: React.MutableRefObject<boolean>
scrollY: Animated.SharedValue<number>
fetchingType: Animated.SharedValue<0 | 1 | 2>
disableRefresh?: boolean
readMarker?: 'read_marker_following'
}
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2)
export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2)
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
fetchingActive,
scrollY,
fetchingType,
disableRefresh = false
disableRefresh = false,
readMarker
}) => {
if (Platform.OS !== 'ios') {
return null
@ -55,7 +60,15 @@ const TimelineRefresh: React.FC<Props> = ({
const prevStatusId = useRef<Mastodon.Status['id']>()
const queryClient = useQueryClient()
const { refetch, isFetching } = useTimelineQuery({ ...queryKey[1] })
const { refetch, isRefetching } = useTimelineQuery({ ...queryKey[1] })
useDerivedValue(() => {
if (prevActive.current || isRefetching) {
fetchingActive.current = true
} else {
fetchingActive.current = false
}
}, [prevActive.current, isRefetching])
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
@ -83,7 +96,7 @@ const TimelineRefresh: React.FC<Props> = ({
const arrowStage = useSharedValue(0)
useAnimatedReaction(
() => {
if (isFetching) {
if (fetchingActive.current) {
return false
}
switch (arrowStage.value) {
@ -116,9 +129,10 @@ const TimelineRefresh: React.FC<Props> = ({
runOnJS(haptics)('Light')
}
},
[isFetching]
[fetchingActive.current]
)
const fetchAndScrolled = useSharedValue(false)
const runFetchPrevious = async () => {
if (prevActive.current) return
@ -134,29 +148,12 @@ const TimelineRefresh: React.FC<Props> = ({
await queryFunctionTimeline({
queryKey,
pageParam: firstPage?.links?.prev && {
...(firstPage.links.prev.isOffset
? { offset: firstPage.links.prev.id }
: { min_id: firstPage.links.prev.id })
},
pageParam: firstPage?.links?.prev,
meta: {}
}).then(res => {
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
prevCache.current = res.body.slice(0, -PREV_PER_BATCH)
return { ...old, pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages] }
})
})
}
useEffect(() => {
const loop = async () => {
for await (const _ of Array(Math.ceil((prevCache.current?.length || 0) / PREV_PER_BATCH))) {
await new Promise(promise => setTimeout(promise, 32))
.then(res => {
if (!res.body.length) return
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
@ -164,33 +161,62 @@ const TimelineRefresh: React.FC<Props> = ({
>(queryKey, old => {
if (!old) return old
prevCache.current = res.body.slice(0, -PREV_PER_BATCH)
return {
...old,
pages: old.pages.map((page, index) => {
if (index === 0) {
const insert = prevCache.current?.slice(-PREV_PER_BATCH)
prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH)
if (insert) {
return { ...page, body: [...insert, ...page.body] }
} else {
return page
}
} else {
return page
}
})
pages: [{ ...res, body: res.body.slice(-PREV_PER_BATCH) }, ...old.pages]
}
})
break
}
prevActive.current = false
}
loop()
}, [prevCache.current])
return res.body.length - PREV_PER_BATCH
})
.then(async nextLength => {
if (!nextLength) {
prevActive.current = false
return
}
for (let [index] of Array(Math.ceil(nextLength / PREV_PER_BATCH)).entries()) {
if (!fetchAndScrolled.value && index < 3 && scrollY.value > 15) {
fetchAndScrolled.value = true
flRef.current?.scrollToOffset({ offset: scrollY.value - 15, animated: true })
}
await new Promise(promise => setTimeout(promise, 32))
queryClient.setQueryData<
InfiniteData<
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
>
>(queryKey, old => {
if (!old) return old
return {
...old,
pages: old.pages.map((page, index) => {
if (index === 0) {
const insert = prevCache.current?.slice(-PREV_PER_BATCH)
prevCache.current = prevCache.current?.slice(0, -PREV_PER_BATCH)
if (insert) {
return { ...page, body: [...insert, ...page.body] }
} else {
return page
}
} else {
return page
}
})
}
})
}
prevActive.current = false
})
}
const runFetchLatest = async () => {
queryClient.invalidateQueries(queryKey)
if (readMarker) {
setAccountStorage([{ key: readMarker, value: undefined }])
}
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
}
@ -224,61 +250,53 @@ const TimelineRefresh: React.FC<Props> = ({
alignItems: 'center'
}}
>
{prevActive.current || isFetching ? (
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
</View>
) : (
<>
<View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.fetchPreviousPage')}
<View style={{ flex: 1, flexDirection: 'row', height: CONTAINER_HEIGHT }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
}
/>
</View>
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.refetch')}
/>
</View>
</>
)}
}
/>
</View>
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Text
style={{
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
}}
children={t('refresh.refetch')}
/>
</View>
</Animated.View>
)
}

View File

@ -4,6 +4,7 @@ import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
import { Platform } from 'expo-modules-core'
import * as ScreenOrientation from 'expo-screen-orientation'
import React, { useRef, useState } from 'react'
import { Pressable, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
@ -72,14 +73,21 @@ const AttachmentVideo: React.FC<Props> = ({
posterStyle: { resizeMode: ResizeMode.COVER }
})}
useNativeControls={false}
onFullscreenUpdate={event => {
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
if (gifv && !reduceMotionEnabled && autoplayGifv) {
videoPlayer.current?.playAsync()
} else {
videoPlayer.current?.pauseAsync()
}
onFullscreenUpdate={async ({ fullscreenUpdate }) => {
switch (fullscreenUpdate) {
case VideoFullscreenUpdate.PLAYER_DID_PRESENT:
Platform.OS === 'android' && (await ScreenOrientation.unlockAsync())
break
case VideoFullscreenUpdate.PLAYER_WILL_DISMISS:
Platform.OS === 'android' &&
(await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT))
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
if (gifv && !reduceMotionEnabled && autoplayGifv) {
videoPlayer.current?.playAsync()
} else {
videoPlayer.current?.pauseAsync()
}
break
}
}}
onPlaybackStatusUpdate={event => {

View File

@ -1,9 +1,14 @@
import ComponentSeparator from '@components/Separator'
import TimelineDefault from '@components/Timeline/Default'
import { useScrollToTop } from '@react-navigation/native'
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { flattenPages } from '@utils/queryHooks/utils'
import { useGlobalStorageListener } from '@utils/storage/actions'
import {
getAccountStorage,
setAccountStorage,
useGlobalStorageListener
} from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useRef } from 'react'
@ -13,7 +18,7 @@ import TimelineEmpty from './Empty'
import TimelineFooter from './Footer'
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<any>)
export interface Props {
flRef?: RefObject<FlatList<any>>
@ -24,7 +29,8 @@ export interface Props {
>
disableRefresh?: boolean
disableInfinity?: boolean
customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'>
readMarker?: 'read_marker_following'
customProps?: Partial<FlatListProps<any>>
}
const Timeline: React.FC<Props> = ({
@ -33,6 +39,7 @@ const Timeline: React.FC<Props> = ({
queryOptions,
disableRefresh = false,
disableInfinity = false,
readMarker = undefined,
customProps
}) => {
const { colors } = useTheme()
@ -45,17 +52,12 @@ const Timeline: React.FC<Props> = ({
notifyOnChangeProps: Platform.select({
ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
}),
getNextPageParam: lastPage =>
lastPage?.links?.next && {
...(lastPage.links.next.isOffset
? { offset: lastPage.links.next.id }
: { max_id: lastPage.links.next.id })
}
})
}
})
const flRef = useRef<FlatList>(null)
const fetchingActive = useRef<boolean>(false)
const scrollY = useSharedValue(0)
const fetchingType = useSharedValue<0 | 1 | 2>(0)
@ -78,6 +80,32 @@ const Timeline: React.FC<Props> = ({
[isFetching]
)
const viewabilityConfigCallbackPairs = useRef<
Pick<FlatListProps<any>, 'viewabilityConfigCallbackPairs'>['viewabilityConfigCallbackPairs']
>(
readMarker
? [
{
viewabilityConfig: {
minimumViewTime: 300,
itemVisiblePercentThreshold: 80,
waitForInteraction: true
},
onViewableItemsChanged: ({ viewableItems }) => {
const marker = readMarker ? getAccountStorage.string(readMarker) : undefined
const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id
if (!fetchingActive.current && firstItemId && firstItemId > (marker || '0')) {
setAccountStorage([{ key: readMarker, value: firstItemId }])
} else {
// setAccountStorage([{ key: readMarker, value: '109519141378761752' }])
}
}
}
]
: undefined
)
const androidRefreshControl = Platform.select({
android: {
refreshControl: (
@ -86,7 +114,12 @@ const Timeline: React.FC<Props> = ({
colors={[colors.primaryDefault]}
progressBackgroundColor={colors.backgroundDefault}
refreshing={isFetching || isLoading}
onRefresh={() => refetch()}
onRefresh={() => {
if (readMarker) {
setAccountStorage([{ key: readMarker, value: undefined }])
}
refetch()
}}
/>
)
}
@ -102,9 +135,11 @@ const Timeline: React.FC<Props> = ({
<TimelineRefresh
flRef={flRef}
queryKey={queryKey}
fetchingActive={fetchingActive}
scrollY={scrollY}
fetchingType={fetchingType}
disableRefresh={disableRefresh}
readMarker={readMarker}
/>
<AnimatedFlatList
ref={customFLRef || flRef}
@ -112,6 +147,9 @@ const Timeline: React.FC<Props> = ({
onScroll={onScroll}
windowSize={7}
data={flattenPages(data)}
{...(customProps?.renderItem
? { renderItem: customProps.renderItem }
: { renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} /> })}
initialNumToRender={6}
maxToRenderPerBatch={3}
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
@ -129,6 +167,7 @@ const Timeline: React.FC<Props> = ({
/>
)
}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
{...(!isLoading && {
maintainVisibleContentPosition: {
minIndexForVisible: 0

View File

@ -2,7 +2,6 @@ import { HeaderRight } from '@components/Header'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useListsQuery } from '@utils/queryHooks/lists'
@ -178,14 +177,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
navigation.setParams({ queryKey: queryKey })
}, [mode, queryKey[1], pageLocal, lists])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} readMarker='read_marker_following' />
}
export default Root

View File

@ -1,5 +1,4 @@
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { TabMeStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
@ -13,14 +12,7 @@ const TabMeBookmarks: React.FC<NativeStackScreenProps<TabMeStackParamList, 'Tab-
navigation.setParams({ queryKey: queryKey })
}, [])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} />
}
export default TabMeBookmarks

View File

@ -1,5 +1,4 @@
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { TabMeStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
@ -13,14 +12,7 @@ const TabMeFavourites: React.FC<
navigation.setParams({ queryKey: queryKey })
}, [])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} />
}
export default TabMeFavourites

View File

@ -23,17 +23,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
const { t } = useTranslation(['common', 'screenTabs'])
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
const { data, refetch, fetchNextPage, hasNextPage } = useListAccountsQuery({
...queryKey[1],
options: {
getNextPageParam: lastPage =>
lastPage?.links?.next && {
...(lastPage.links.next.isOffset
? { offset: lastPage.links.next.id }
: { max_id: lastPage.links.next.id })
}
}
})
const { data, refetch, fetchNextPage, hasNextPage } = useListAccountsQuery({ ...queryKey[1] })
const mutation = useListAccountsMutation({
onSuccess: () => {

View File

@ -1,7 +1,6 @@
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { useQueryClient } from '@tanstack/react-query'
import { TabMeStackParamList } from '@utils/navigation/navigators'
@ -74,14 +73,7 @@ const TabMeList: React.FC<NativeStackScreenProps<TabMeStackParamList, 'Tab-Me-Li
navigation.setParams({ queryKey })
}, [list])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} />
}
export default TabMeList

View File

@ -1,6 +1,5 @@
import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import SegmentedControl from '@react-native-community/segmented-control'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
@ -21,15 +20,7 @@ const Route = ({ route: { key: page } }: { route: any }) => {
useEffect(() => {
navigation.setParams({ queryKey })
}, [])
return (
<Timeline
queryKey={queryKey}
disableRefresh={page === 'Trending'}
customProps={{
renderItem: ({ item }: any) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} disableRefresh={page === 'Trending'} />
}
const renderScene = SceneMap({

View File

@ -2,7 +2,6 @@ import { HeaderLeft } from '@components/Header'
import { ParseEmojis } from '@components/Parse'
import CustomText from '@components/Text'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager'
@ -49,14 +48,7 @@ const TabSharedAttachments: React.FC<TabSharedStackScreenProps<'Tab-Shared-Attac
{ page: 'Account', id: account.id, exclude_reblogs: true, only_media: true }
]
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} />
}
export default TabSharedAttachments

View File

@ -2,7 +2,6 @@ import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
@ -82,14 +81,7 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
})
}, [canFollowTags, data?.following, isFetching])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
return <Timeline queryKey={queryKey} />
}
export default TabSharedHashtag

View File

@ -74,7 +74,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
const match = urlMatcher(toot.url || toot.uri)
const highlightIndex = useRef<number>(0)
const query = useQuery<{ pages: { body: (Mastodon.Status & { key?: 'cached' })[] }[] }>(
const query = useQuery<{ pages: { body: Mastodon.Status[] }[] }>(
queryKey.local,
async () => {
const context = await apiInstance<{
@ -94,11 +94,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
{
body: statuses.map((status, index) => {
if (index < highlightIndex.current || status.id === toot.id) {
return { ...status, _level: 0 }
status._level = 0
return status
} else {
const repliedLevel: number =
statuses.find(s => s.id === status.in_reply_to_id)?._level || 0
return { ...status, _level: repliedLevel + 1 }
status._level = repliedLevel + 1
return status
}
})
}
@ -106,7 +108,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}
},
{
initialData: { pages: [{ body: [{ ...toot, _level: 0, key: 'cached' }] }] },
placeholderData: { pages: [{ body: [toot] }] },
enabled: !toot._remote,
staleTime: 0,
refetchOnMount: true,
@ -169,11 +171,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
return statuses.map((status, index) => {
if (index < highlightIndex.current || status.id === toot.id) {
return { ...status, _level: 0 }
status._level = 0
return status
}
const repliedLevel: number = statuses.find(s => s.id === status.in_reply_to_id)?._level || 0
return { ...status, _level: repliedLevel + 1 }
status._level = repliedLevel + 1
return status
})
},
{
@ -184,12 +188,12 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
staleTime: 0,
refetchOnMount: true,
onSuccess: data => {
if (query.data.pages[0].body.length < 1 && data.length < 1) {
if ((query.data?.pages[0].body.length || 0) < 1 && data.length < 1) {
navigation.goBack()
return
}
if (query.data.pages[0].body.length < data.length) {
if ((query.data?.pages[0].body.length || 0) < data.length) {
queryClient.cancelQueries(queryKey.local)
queryClient.setQueryData<{
pages: { body: Mastodon.Status[] }[]
@ -201,12 +205,11 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
pages: [
{
body: data.map(remote => {
const localMatch = query.data.pages[0].body.find(
const localMatch = query.data?.pages[0].body.find(
local => local.uri === remote.uri
)
if (localMatch) {
delete localMatch.key
return localMatch
return { ...localMatch, _level: remote._level }
} else {
return {
...remote,
@ -265,9 +268,9 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
windowSize={7}
data={query.data?.pages?.[0].body}
renderItem={({ item, index }) => {
const prev = query.data.pages[0].body[index - 1]?._level || 0
const prev = query.data?.pages[0].body[index - 1]?._level || 0
const curr = item._level
const next = query.data.pages[0].body[index + 1]?._level || 0
const next = query.data?.pages[0].body[index + 1]?._level || 0
return (
<View
@ -373,7 +376,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
})
: null}
{/* <CustomText
children={finalData.current[index - 1]?._level}
children={query.data?.pages[0].body[index - 1]?._level}
style={{ position: 'absolute', top: 4, left: 4, color: colors.red }}
/>
<CustomText
@ -381,7 +384,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
style={{ position: 'absolute', top: 20, left: 4, color: colors.yellow }}
/>
<CustomText
children={finalData.current[index + 1]?._level}
children={query.data?.pages[0].body[index + 1]?._level}
style={{ position: 'absolute', top: 36, left: 4, color: colors.green }}
/> */}
</View>
@ -421,7 +424,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })
try {
error.index < query.data.pages[0].body.length &&
error.index < (query.data?.pages[0].body.length || 0) &&
setTimeout(
() =>
flRef.current?.scrollToIndex({

View File

@ -33,12 +33,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
const queryKey: QueryKeyUsers = ['Users', params]
const { data, isFetching, hasNextPage, fetchNextPage, isFetchingNextPage } = useUsersQuery({
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage.links?.prev?.id && { min_id: firstPage.links.prev.id },
getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id }
}
...queryKey[1]
})
const [isSearching, setIsSearching] = useState(false)

View File

@ -1,5 +1,5 @@
import axios from 'axios'
import { ctx, handleError, PagedResponse, userAgent } from './helpers'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
@ -49,29 +49,7 @@ const apiGeneral = async <T = unknown>({
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
})
.then(response => {
let links: {
prev?: { id: string; isOffset: boolean }
next?: { id: string; isOffset: boolean }
} = {}
if (response.headers?.link) {
const linksParsed = response.headers.link.matchAll(
new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
)
for (const link of linksParsed) {
switch (link[3]) {
case 'prev':
links.prev = { id: link[2], isOffset: link[1].includes('offset') }
break
case 'next':
links.next = { id: link[2], isOffset: link[1].includes('offset') }
break
}
}
}
return Promise.resolve({ body: response.data, links })
})
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))
.catch(handleError())
}

View File

@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react-native'
import chalk from 'chalk'
import Constants from 'expo-constants'
import { Platform } from 'react-native'
import parse from 'url-parse'
const userAgent = {
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
@ -64,10 +65,42 @@ const handleError =
}
}
export const parseHeaderLinks = (headerLink?: string): PagedResponse['links'] => {
if (!headerLink) return undefined
const links: PagedResponse['links'] = {}
const linkParsed = [...headerLink.matchAll(/<(\S+?)>; *rel="(next|prev)"/gi)]
for (const link of linkParsed) {
const queries = parse(link[1], true).query
const isOffset = !!queries.offset?.length
switch (link[2]) {
case 'prev':
const prevId = isOffset ? queries.offset : queries.min_id
if (prevId) links.prev = isOffset ? { offset: prevId } : { min_id: prevId }
break
case 'next':
const nextId = isOffset ? queries.offset : queries.max_id
if (nextId) links.next = isOffset ? { offset: nextId } : { max_id: nextId }
break
}
}
if (links.prev || links.next) {
return links
} else {
return undefined
}
}
type LinkFormat = { id: string; isOffset: boolean }
export type PagedResponse<T = unknown> = {
body: T
links?: { prev?: LinkFormat; next?: LinkFormat }
links?: {
prev?: { min_id: string } | { offset: string }
next?: { max_id: string } | { offset: string }
}
}
export { ctx, handleError, userAgent }

View File

@ -1,6 +1,6 @@
import { getAccountDetails } from '@utils/storage/actions'
import axios, { AxiosRequestConfig } from 'axios'
import { ctx, handleError, PagedResponse, userAgent } from './helpers'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
export type Params = {
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
@ -57,29 +57,7 @@ const apiInstance = async <T = unknown>({
...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
...extras
})
.then(response => {
let links: {
prev?: { id: string; isOffset: boolean }
next?: { id: string; isOffset: boolean }
} = {}
if (response.headers?.link) {
const linksParsed = response.headers.link.matchAll(
new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
)
for (const link of linksParsed) {
switch (link[3]) {
case 'prev':
links.prev = { id: link[2], isOffset: link[1].includes('offset') }
break
case 'next':
links.next = { id: link[2], isOffset: link[1].includes('offset') }
break
}
}
}
return Promise.resolve({ body: response.data, links })
})
.then(response => ({ body: response.data, links: parseHeaderLinks(response.headers.link) }))
.catch(handleError())
}

View File

@ -1,15 +1,16 @@
import {
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from '@tanstack/react-query'
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
import { AxiosError } from 'axios'
import { infinitePageParams } from './utils'
export type QueryKeyLists = ['Lists']
@ -98,10 +99,16 @@ const useListAccountsQuery = ({
options,
...queryKeyParams
}: QueryKeyListAccounts[1] & {
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Account[]>, AxiosError>
options?: Omit<
UseInfiniteQueryOptions<PagedResponse<Mastodon.Account[]>, AxiosError>,
'getPreviousPageParam' | 'getNextPageParam'
>
}) => {
const queryKey: QueryKeyListAccounts = ['ListAccounts', queryKeyParams]
return useInfiniteQuery(queryKey, accountsQueryFunction, options)
return useInfiniteQuery(queryKey, accountsQueryFunction, {
...options,
...infinitePageParams
})
}
type AccountsMutationVarsLists = {

View File

@ -18,6 +18,7 @@ import { searchLocalStatus } from './search'
import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem'
import updateStatusProperty from './timeline/updateStatusProperty'
import { infinitePageParams } from './utils'
export type QueryKeyTimeline = [
'Timeline',
@ -57,7 +58,25 @@ export const queryFunctionTimeline = async ({
pageParam
}: QueryFunctionContext<QueryKeyTimeline>) => {
const page = queryKey[1]
let params: { [key: string]: string } = { limit: 40, ...pageParam }
let marker: string | undefined
if (page.page === 'Following' && !pageParam?.offset && !pageParam?.min_id && !pageParam?.max_id) {
const storedMarker = getAccountStorage.string('read_marker_following')
if (storedMarker) {
await apiInstance<Mastodon.Status[]>({
method: 'get',
url: 'timelines/home',
params: { limit: 1, min_id: storedMarker }
}).then(res => {
if (res.body.length) {
marker = storedMarker
}
})
}
}
let params: { [key: string]: string } = marker
? { limit: 40, max_id: marker }
: { limit: 40, ...pageParam }
switch (page.page) {
case 'Following':
@ -137,7 +156,16 @@ export const queryFunctionTimeline = async ({
case 'Account':
if (!page.id) return Promise.reject('Timeline query account id not provided')
if (page.exclude_reblogs) {
if (page.only_media) {
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
only_media: 'true',
...params
}
})
} else if (page.exclude_reblogs) {
if (pageParam && pageParam.hasOwnProperty('max_id')) {
return apiInstance<Mastodon.Status[]>({
method: 'get',
@ -177,8 +205,8 @@ export const queryFunctionTimeline = async ({
url: `accounts/${page.id}/statuses`,
params: {
...params,
exclude_replies: page.exclude_reblogs.toString(),
only_media: page.only_media.toString()
exclude_replies: false,
only_media: false
}
})
}
@ -228,14 +256,18 @@ const useTimelineQuery = ({
options,
...queryKeyParams
}: QueryKeyTimeline[1] & {
options?: UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>
options?: Omit<
UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>,
'getPreviousPageParam' | 'getNextPageParam'
>
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunctionTimeline, {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
...options
...options,
...infinitePageParams
})
}
@ -431,7 +463,6 @@ const useTimelineMutation = ({
updateStatusProperty(params, navigationState)
break
case 'editItem':
console.log('YES!!!')
editItem(params)
break
case 'deleteItem':

View File

@ -9,6 +9,7 @@ import apiInstance from '@utils/api/instance'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import { AxiosError } from 'axios'
import { infinitePageParams } from './utils'
export type QueryKeyUsers = ['Users', TabSharedStackParamList['Tab-Shared-Users']]
@ -73,13 +74,19 @@ const useUsersQuery = ({
options,
...queryKeyParams
}: QueryKeyUsers[1] & {
options?: UseInfiniteQueryOptions<
PagedResponse<Mastodon.Account[]> & { warnIncomplete: boolean; remoteData: boolean },
AxiosError
options?: Omit<
UseInfiniteQueryOptions<
PagedResponse<Mastodon.Account[]> & { warnIncomplete: boolean; remoteData: boolean },
AxiosError
>,
'getPreviousPageParam' | 'getNextPageParam'
>
}) => {
const queryKey: QueryKeyUsers = ['Users', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunction, options)
return useInfiniteQuery(queryKey, queryFunction, {
...options,
...infinitePageParams
})
}
export { useUsersQuery }

View File

@ -2,10 +2,8 @@ import { InfiniteData } from '@tanstack/react-query'
import { PagedResponse } from '@utils/api/helpers'
export const infinitePageParams = {
getPreviousPageParam: (firstPage: PagedResponse<any>) =>
firstPage.links?.prev && { min_id: firstPage.links.next },
getNextPageParam: (lastPage: PagedResponse<any>) =>
lastPage.links?.next && { max_id: lastPage.links.next }
getPreviousPageParam: (firstPage: PagedResponse<any>) => firstPage.links?.prev,
getNextPageParam: (lastPage: PagedResponse<any>) => lastPage.links?.next
}
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>

View File

@ -24,6 +24,7 @@ export type AccountV0 = {
'auth.account.domain': string // used for username
'auth.account.avatar_static': string
version: string
read_marker_following?: string
// number
// boolean
// object

View File

@ -1,5 +1,6 @@
import { queryClient } from '@utils/queryHooks'
import { storage } from '@utils/storage'
import { Platform } from 'react-native'
import {
MMKV,
useMMKVBoolean,
@ -40,10 +41,21 @@ export const useGlobalStorage = {
useMMKVString(key, storage.global) as NonNullable<StorageGlobal[T]> extends string
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never,
number: <T extends keyof StorageGlobal>(key: T) =>
useMMKVNumber(key, storage.global) as NonNullable<StorageGlobal[T]> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never,
number: <T extends keyof StorageGlobal>(key: T) => {
if (Platform.OS === 'ios') {
return useMMKVString(key, storage.global) as NonNullable<StorageGlobal[T]> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never
} else {
const [num, setNum] = useMMKVString(key, storage.global)
// @ts-ignore
return [parseInt(num), v => setNum(v.toString())] as NonNullable<
StorageGlobal[T]
> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never
}
},
boolean: <T extends keyof StorageGlobal>(key: T) =>
useMMKVBoolean(key, storage.global) as NonNullable<StorageGlobal[T]> extends boolean
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]

View File

@ -1476,7 +1476,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.8.4":
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.8.4":
version: 7.20.7
resolution: "@babel/runtime@npm:7.20.7"
dependencies:
@ -2744,12 +2744,12 @@ __metadata:
languageName: node
linkType: hard
"@react-native-camera-roll/camera-roll@npm:^5.2.0":
version: 5.2.0
resolution: "@react-native-camera-roll/camera-roll@npm:5.2.0"
"@react-native-camera-roll/camera-roll@npm:^5.2.1":
version: 5.2.1
resolution: "@react-native-camera-roll/camera-roll@npm:5.2.1"
peerDependencies:
react-native: ">=0.59"
checksum: 27800224bdbd128800e6a64046d76865814a43215ad3a0df9a93eeac64b523e3b68382d7cabb79b574e354d03dddc625b93fa2b6010ed04f6a89296038e3eabf
checksum: db901554170cced81db4b01c0e89324905fe4c6915a41e4cf4b6d1fa35f5894d18d1d734304d516824bff434784e4b571e9fa379e850f8dd4d42a28e3eab9eee
languageName: node
linkType: hard
@ -5692,13 +5692,13 @@ __metadata:
languageName: node
linkType: hard
"expo-modules-core@npm:1.1.0":
version: 1.1.0
resolution: "expo-modules-core@npm:1.1.0"
"expo-modules-core@npm:1.1.1":
version: 1.1.1
resolution: "expo-modules-core@npm:1.1.1"
dependencies:
compare-versions: ^3.4.0
invariant: ^2.2.4
checksum: f5add659b2f43c2784dce86986ec709feac38a60881afd013049ef86b106eeba9217427d01b9da27e00b678e9f47518ffa5d7676f95408c6e3966093d48252e8
checksum: 6a2b6b5d1f56f197ddf8b3f3a638d8f15534657218bdf895bd96c07cb34fc8404761d6a9206527fa5b67baade60858b4530de147fe1f3b7db8205c5d9d059b31
languageName: node
linkType: hard
@ -5741,6 +5741,15 @@ __metadata:
languageName: node
linkType: hard
"expo-screen-orientation@npm:^5.0.1":
version: 5.0.1
resolution: "expo-screen-orientation@npm:5.0.1"
peerDependencies:
expo: "*"
checksum: 7ede30533a8c492f82b58c3b8be110b6373ffcc2cbe273299d9f15d9aa943d678d8aaffb3d2565780b45d1d5a2a1ddea54d813fc84c06e30e3cfd59abbd8e30e
languageName: node
linkType: hard
"expo-secure-store@npm:^12.0.0":
version: 12.0.0
resolution: "expo-secure-store@npm:12.0.0"
@ -5791,9 +5800,9 @@ __metadata:
languageName: node
linkType: hard
"expo@npm:^47.0.11":
version: 47.0.11
resolution: "expo@npm:47.0.11"
"expo@npm:^47.0.12":
version: 47.0.12
resolution: "expo@npm:47.0.12"
dependencies:
"@babel/runtime": ^7.14.0
"@expo/cli": 0.4.11
@ -5810,7 +5819,7 @@ __metadata:
expo-font: ~11.0.1
expo-keep-awake: ~11.0.1
expo-modules-autolinking: 1.0.1
expo-modules-core: 1.1.0
expo-modules-core: 1.1.1
fbemitter: ^3.0.0
getenv: ^1.0.0
invariant: ^2.2.4
@ -5823,7 +5832,7 @@ __metadata:
optional: true
bin:
expo: bin/cli.js
checksum: 5e470d7b0c94ddade3f77e4e5038f025635f432f1e56a9e541455f5e71b5f4919c7c756639b5bf31ff6b877ad254a77290efe0305d48712e5406c76f3c8b7e77
checksum: 24f7073660fb4c4c76a398d722d1fcae1ea654463faf2a730e986f884618a93618d1ee9116fe6c8199338c2e8aa716c2d389344a967824d264e2440eb7f16340
languageName: node
linkType: hard
@ -9416,11 +9425,11 @@ __metadata:
languageName: node
linkType: hard
"react-i18next@npm:^12.1.1":
version: 12.1.1
resolution: "react-i18next@npm:12.1.1"
"react-i18next@npm:^12.1.4":
version: 12.1.4
resolution: "react-i18next@npm:12.1.4"
dependencies:
"@babel/runtime": ^7.14.5
"@babel/runtime": ^7.20.6
html-parse-stringify: ^3.0.1
peerDependencies:
i18next: ">= 19.0.0"
@ -9430,7 +9439,7 @@ __metadata:
optional: true
react-native:
optional: true
checksum: baeb1957d70281d1a95ef0b94801bc2e6bf17066a49a45f32cd1a706ead3ddce2ab3e7b321e07c43f082efa9987c2d55856a5bdf419d9ad628dbfa9ec87af8ea
checksum: 6f0f8a47f0bf7da2c9ac383c88b6ef02446d9d1aa2609cfcc98d8a28999da85361f065fbe7f7f4af910df4e8d53af7879db4b9cd681274d582fc7bdd1b07813b
languageName: node
linkType: hard
@ -11254,7 +11263,7 @@ __metadata:
"@mattermost/react-native-paste-input": ^0.5.2
"@neverdull-agency/expo-unlimited-secure-store": ^1.0.10
"@react-native-async-storage/async-storage": ~1.17.11
"@react-native-camera-roll/camera-roll": ^5.2.0
"@react-native-camera-roll/camera-roll": ^5.2.1
"@react-native-clipboard/clipboard": ^1.11.1
"@react-native-community/blur": ^4.3.0
"@react-native-community/netinfo": 9.3.7
@ -11281,7 +11290,7 @@ __metadata:
chalk: ^4.1.2
diff: ^5.1.0
dotenv: ^16.0.3
expo: ^47.0.11
expo: ^47.0.12
expo-auth-session: ^3.8.0
expo-av: ^13.1.0
expo-constants: ^14.1.0
@ -11293,6 +11302,7 @@ __metadata:
expo-notifications: ^0.17.0
expo-random: ^13.0.0
expo-screen-capture: ^5.0.0
expo-screen-orientation: ^5.0.1
expo-secure-store: ^12.0.0
expo-splash-screen: ^0.17.5
expo-store-review: ^6.1.0
@ -11304,7 +11314,7 @@ __metadata:
lodash: ^4.17.21
react: ^18.2.0
react-dom: ^18.2.0
react-i18next: ^12.1.1
react-i18next: ^12.1.4
react-intl: ^6.2.5
react-native: ^0.70.6
react-native-animated-spinkit: ^1.5.2