mirror of https://github.com/tooot-app/app
Merge branch 'main' into candidate
This commit is contained in:
commit
a60fb546d8
|
@ -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 ->
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
toooting愉快!此版本包括以下改进和修复:
|
||||
- 主动获取对话的远程内容
|
||||
- 自动加载上次我的关注的阅读位置
|
||||
- 可添加举报细节
|
||||
- 新增暂停自动播放gif动画选项
|
||||
- 隐藏用户的转嘟
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,13 +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 => {
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.body.length) return
|
||||
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<
|
||||
PagedResponse<(Mastodon.Status | Mastodon.Notification | Mastodon.Conversation)[]>
|
||||
|
@ -149,13 +162,26 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
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] }
|
||||
})
|
||||
})
|
||||
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))) {
|
||||
})
|
||||
|
||||
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<
|
||||
|
@ -181,16 +207,16 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
prevActive.current = false
|
||||
})
|
||||
}
|
||||
loop()
|
||||
}, [prevCache.current])
|
||||
|
||||
const runFetchLatest = async () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
if (readMarker) {
|
||||
setAccountStorage([{ key: readMarker, value: undefined }])
|
||||
}
|
||||
await refetch()
|
||||
setTimeout(() => flRef.current?.scrollToOffset({ offset: 0 }), 50)
|
||||
}
|
||||
|
@ -224,12 +250,6 @@ 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={{
|
||||
|
@ -277,8 +297,6 @@ const TimelineRefresh: React.FC<Props> = ({
|
|||
children={t('refresh.refetch')}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
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 = {
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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<
|
||||
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 }
|
||||
|
|
|
@ -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[] | [] =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
: 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]
|
||||
|
|
54
yarn.lock
54
yarn.lock
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue