This commit is contained in:
xmflsct 2022-12-31 00:07:28 +01:00
parent 49a0e6d63e
commit b677c4b7ce
17 changed files with 290 additions and 166 deletions

View File

@ -1,8 +1,4 @@
import { registerRootComponent } from 'expo'
import App from './src/App'
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in the Expo client or in a native build,
// the environment is set up appropriately
registerRootComponent(App)

View File

@ -1,7 +1,7 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
import { StyleProp, View, ViewStyle } from 'react-native'
export interface Props {
extraMarginLeft?: number
@ -23,7 +23,7 @@ const ComponentSeparator: React.FC<Props> = ({
{
backgroundColor: colors.backgroundDefault,
borderTopColor: colors.border,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopWidth: 1,
marginLeft: StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
marginRight: StyleConstants.Spacing.Global.PagePadding + extraMarginRight
}

View File

@ -329,8 +329,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
marginHorizontal: StyleConstants.Spacing.S
paddingVertical: StyleConstants.Spacing.S * 1.5
}
})

View File

@ -10,7 +10,7 @@ import { useStatusQuery } from '@utils/queryHooks/status'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import { Pressable, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import TimelineDefault from '../Default'
import StatusContext from './Context'
@ -192,7 +192,7 @@ const TimelineCard: React.FC = () => {
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S,
overflow: 'hidden',
borderColor: colors.border

View File

@ -2,6 +2,7 @@ import ComponentSeparator from '@components/Separator'
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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -54,8 +55,6 @@ const Timeline: React.FC<Props> = ({
}
})
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
const flRef = useRef<FlatList>(null)
const scrollY = useSharedValue(0)
@ -112,7 +111,7 @@ const Timeline: React.FC<Props> = ({
scrollEventThrottle={16}
onScroll={onScroll}
windowSize={7}
data={flattenData}
data={flattenPages(data)}
initialNumToRender={6}
maxToRenderPerBatch={3}
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}

View File

@ -203,7 +203,7 @@ const menuStatus = ({
}),
disabled: false,
destructive: false,
hidden: !ownAccount && !status.mentions.filter(mention => mention.id === accountId).length
hidden: !ownAccount && !status.mentions?.filter(mention => mention.id === accountId).length
},
title: t('componentContextMenu:status.mute.action', {
defaultValue: 'false',

View File

@ -2,7 +2,7 @@ import TimelineDefault from '@components/Timeline/Default'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native'
import { View } from 'react-native'
import ComposeContext from '../../utils/createContext'
const ComposeReply: React.FC = () => {
@ -16,7 +16,7 @@ const ComposeReply: React.FC = () => {
style={{
flex: 1,
flexDirection: 'row',
borderWidth: StyleSheet.hairlineWidth,
borderWidth: 1,
borderRadius: StyleConstants.Spacing.S,
overflow: 'hidden',
borderColor: colors.border,

View File

@ -5,6 +5,7 @@ import { displayMessage } from '@components/Message'
import ComponentSeparator from '@components/Separator'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
import { flattenPages } from '@utils/queryHooks/utils'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native-gesture-handler'
@ -15,7 +16,8 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
const { t } = useTranslation(['common', 'screenTabs'])
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
const flattenData = flattenPages(data)
useEffect(() => {
if (flattenData.length === 0) {
navigation.goBack()

View File

@ -9,6 +9,7 @@ import {
useListAccountsMutation,
useListAccountsQuery
} from '@utils/queryHooks/lists'
import { flattenPages } from '@utils/queryHooks/utils'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
@ -18,7 +19,7 @@ import { FlatList, View } from 'react-native'
const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> = ({
route: { params }
}) => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const { t } = useTranslation(['common', 'screenTabs'])
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
@ -34,8 +35,6 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
}
})
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
const mutation = useListAccountsMutation({
onSuccess: () => {
haptics('Light')
@ -53,7 +52,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
return (
<FlatList
data={flattenData}
data={flattenPages(data)}
renderItem={({ item, index }) => (
<ComponentAccount
key={index}

View File

@ -89,7 +89,7 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
color: (fontSize || 0) === size ? colors.primaryDefault : colors.secondary,
borderWidth: StyleSheet.hairlineWidth,
borderWidth: 1,
borderColor: colors.border
}}
fontWeight={(fontSize || 0) === size ? 'Bold' : undefined}

View File

@ -6,7 +6,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native'
import { KeyboardAvoidingView, Platform, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
const TabMeSwitch: React.FC = () => {
@ -49,7 +49,7 @@ const TabMeSwitch: React.FC = () => {
marginTop: StyleConstants.Spacing.S,
paddingTop: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopWidth: 1,
borderTopColor: colors.border
}}
>

View File

@ -4,6 +4,7 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { flattenPages } from '@utils/queryHooks/utils'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
@ -32,12 +33,9 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
only_media: true
})
const flattenData = data?.pages
? data.pages
.flatMap(d => [...d.body])
.filter(status => !(status as Mastodon.Status).sensitive)
.splice(0, DISPLAY_AMOUNT)
: []
const flattenData = flattenPages(data)
.filter(status => !(status as Mastodon.Status).sensitive)
.splice(0, DISPLAY_AMOUNT)
const styleContainer = useAnimatedStyle(() => {
if (flattenData.length) {

View File

@ -1,14 +1,15 @@
import { HeaderLeft } from '@components/Header'
import ComponentSeparator from '@components/Separator'
import Timeline from '@components/Timeline'
import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTootQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import React, { useEffect, useRef, useState } from 'react'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, View } from 'react-native'
import { Path, Svg } from 'react-native-svg'
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
navigation,
@ -16,6 +17,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
params: { toot, rootQueryKey }
}
}) => {
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
useEffect(() => {
@ -25,55 +27,23 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
})
}, [])
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
const flRef = useRef<FlatList>(null)
const [itemsLength, setItemsLength] = useState(0)
const scrolled = useRef(false)
const queryClient = useQueryClient()
const observer = new InfiniteQueryObserver(queryClient, {
queryKey,
enabled: false
})
const replyLevels = useRef<{ id: string; level: number }[]>([])
const data = useRef<Mastodon.Status[]>()
const highlightIndex = useRef<number>(0)
useEffect(() => {
return observer.subscribe(result => {
if (result.isSuccess) {
const flattenData = result.data?.pages
? // @ts-ignore
result.data.pages.flatMap(d => [...d.body])
: []
// Auto go back when toot page is empty
if (flattenData.length < 1) {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
const { data } = useTootQuery({
...queryKey[1],
options: {
meta: { toot },
onSuccess: data => {
if (data.body.length < 1) {
navigation.goBack()
return
}
data.current = flattenData
highlightIndex.current = flattenData.findIndex(({ id }) => id === toot.id)
for (const [index, status] of flattenData.entries()) {
if (status.id === toot.id) continue
if (status.in_reply_to_id === toot.id) continue
if (!replyLevels.current.find(reply => reply.id === status.in_reply_to_id)) {
const prevLevel =
replyLevels.current.find(reply => reply.id === flattenData[index - 1].in_reply_to_id)
?.level || 0
replyLevels.current.push({
id: status.in_reply_to_id,
level: prevLevel + 1
})
}
}
setItemsLength(flattenData.length)
if (!scrolled.current) {
scrolled.current = true
const pointer = flattenData.findIndex(({ id }) => id === toot.id)
const pointer = data.body.findIndex(({ id }) => id === toot.id)
if (pointer < 1) return
const length = flRef.current?.props.data?.length
if (!length) return
@ -91,78 +61,188 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}
}
}
})
}, [scrolled.current, replyLevels.current])
}
})
const heights = useRef<(number | undefined)[]>([])
return (
<Timeline
flRef={flRef}
queryKey={queryKey}
queryOptions={{ staleTime: 0, refetchOnMount: true }}
customProps={{
ItemSeparatorComponent: ({ leadingItem }) => {
const levels = {
current:
replyLevels.current.find(reply => reply.id === leadingItem.in_reply_to_id)?.level || 0
}
return (
<FlatList
ref={flRef}
scrollEventThrottle={16}
windowSize={7}
data={data?.body}
renderItem={({ item, index }) => {
const MAX_LEVEL = 10
const ARC = StyleConstants.Avatar.XS / 4
const prev = data?.body[index - 1]?._level || 0
const curr = item._level
const next = data?.body[index + 1]?._level || 0
return (
<View
style={{
paddingLeft:
index > (data?.highlightIndex || 0)
? Math.min(item._level, MAX_LEVEL) * StyleConstants.Spacing.S
: undefined
}}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => (heights.current[index] = height)}
>
<TimelineDefault
item={item}
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={toot.id === item.id}
isConversation={toot.id !== item.id}
/>
{curr > 1 || next > 1
? [...new Array(curr)].map((_, i) => {
if (i > MAX_LEVEL) return null
const lastLine = curr === i + 1
if (lastLine) {
if (curr === prev + 1 || curr === next - 1) {
if (curr > next) {
return null
}
return (
<Svg key={i} style={{ position: 'absolute' }}>
<Path
d={
`M ${curr * StyleConstants.Spacing.S + ARC} ${
StyleConstants.Spacing.M + StyleConstants.Avatar.XS / 2
} ` +
`a ${ARC} ${ARC} 0 0 0 -${ARC} ${ARC} ` +
`v 999`
}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}
/>
</Svg>
)
} else {
if (i >= curr - 2) return null
return (
<Svg key={i} style={{ position: 'absolute' }}>
<Path
d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${
(heights.current[index] || 999) -
(StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size.L) / 2 -
StyleConstants.Avatar.XS / 2
} ` +
`a ${ARC} ${ARC} 0 0 0 ${ARC} ${ARC}`
}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}
/>
</Svg>
)
}
} else {
if (i >= next - 1) {
return (
<Svg key={i} style={{ position: 'absolute' }}>
<Path
d={
`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` +
`v ${
(heights.current[index] || 999) -
(StyleConstants.Spacing.S * 1.5 +
StyleConstants.Font.Size.L * 1.35) /
2
} ` +
`h ${ARC}`
}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}
/>
</Svg>
)
} else {
return (
<Svg key={i} style={{ position: 'absolute' }}>
<Path
d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 999`}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}
/>
</Svg>
)
}
}
})
: null}
{/* <CustomText
children={data?.body[index - 1]?._level}
style={{ position: 'absolute', top: 4, left: 4, color: colors.red }}
/>
<CustomText
children={item._level}
style={{ position: 'absolute', top: 20, left: 4, color: colors.yellow }}
/>
<CustomText
children={data?.body[index + 1]?._level}
style={{ position: 'absolute', top: 36, left: 4, color: colors.green }}
/> */}
</View>
)
}}
initialNumToRender={6}
maxToRenderPerBatch={3}
ItemSeparatorComponent={({ leadingItem }) => {
return (
<>
<ComponentSeparator
extraMarginLeft={
toot.id === leadingItem.id
? 0
: StyleConstants.Avatar.XS +
StyleConstants.Spacing.S +
Math.max(0, levels.current - 1) * 8
Math.max(0, leadingItem._level - 1) * 8
}
/>
)
},
renderItem: ({ item, index }) => {
const levels = {
previous:
replyLevels.current.find(
reply => reply.id === data.current?.[index - 1]?.in_reply_to_id
)?.level || 0,
current:
replyLevels.current.find(reply => reply.id === item.in_reply_to_id)?.level || 0,
next:
replyLevels.current.find(
reply => reply.id === data.current?.[index + 1]?.in_reply_to_id
)?.level || 0
}
return (
<View
style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}
>
<TimelineDefault
item={item}
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={toot.id === item.id}
isConversation={toot.id !== item.id}
/>
</View>
)
},
onScrollToIndexFailed: error => {
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })
try {
error.index < itemsLength &&
setTimeout(
() =>
flRef.current?.scrollToIndex({
index: error.index,
viewOffset: 100
}),
500
)
} catch {}
}
{leadingItem._level > 1
? [...new Array(leadingItem._level - 1)].map((_, i) => (
<Svg key={i} style={{ position: 'absolute', top: -1 }}>
<Path
d={`M ${(i + 1) * StyleConstants.Spacing.S} 0 ` + `v 1`}
strokeWidth={1}
stroke={colors.border}
strokeOpacity={0.6}
/>
</Svg>
))
: null}
</>
)
}}
onScrollToIndexFailed={error => {
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })
try {
error.index < (data?.body.length || 0) &&
setTimeout(
() =>
flRef.current?.scrollToIndex({
index: error.index,
viewOffset: 100
}),
500
)
} catch {}
}}
disableRefresh
disableInfinity
/>
)
}

View File

@ -7,6 +7,7 @@ import apiInstance from '@utils/api/instance'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { SearchResult } from '@utils/queryHooks/search'
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
import { flattenPages } from '@utils/queryHooks/utils'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
@ -39,14 +40,13 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
getNextPageParam: lastPage => lastPage.links?.next?.id && { max_id: lastPage.links.next.id }
}
})
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
const [isSearching, setIsSearching] = useState(false)
return (
<FlatList
windowSize={7}
data={flattenData}
data={flattenPages(data)}
style={{
minHeight: '100%',
paddingVertical: StyleConstants.Spacing.Global.PagePadding

View File

@ -1,10 +1,12 @@
import haptics from '@components/haptics'
import {
MutationOptions,
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation
MutationOptions,
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation,
useQuery,
UseQueryOptions
} from '@tanstack/react-query'
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
@ -17,6 +19,67 @@ import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem'
import updateStatusProperty from './timeline/updateStatusProperty'
const queryFunctionToot = async ({ queryKey, meta }: QueryFunctionContext<QueryKeyTimeline>) => {
// @ts-ignore
const id = queryKey[1].toot
const target =
(meta?.toot as Mastodon.Status) ||
undefined ||
(await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${id}`
}).then(res => res.body))
const context = await apiInstance<{
ancestors: Mastodon.Status[]
descendants: Mastodon.Status[]
}>({
method: 'get',
url: `statuses/${id}/context`
})
const statuses: (Mastodon.Status & { _level?: number })[] = [
...context.body.ancestors,
target,
...context.body.descendants
]
const highlightIndex = context.body.ancestors.length
for (const [index, status] of statuses.entries()) {
if (index < highlightIndex || status.id === id) {
statuses[index]._level = 0
continue
}
const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level
statuses[index]._level = (repliedLevel || 0) + 1
}
return { body: statuses, highlightIndex }
}
const useTootQuery = ({
options,
...queryKeyParams
}: QueryKeyTimeline[1] & {
options?: UseQueryOptions<
{
body: (Mastodon.Status & { _level: number })[]
highlightIndex: number
},
AxiosError
>
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useQuery(queryKey, queryFunctionToot, {
staleTime: 0,
refetchOnMount: true,
...options
})
}
/* ----- */
export type QueryKeyTimeline = [
'Timeline',
(
@ -36,16 +99,16 @@ export type QueryKeyTimeline = [
page: 'List'
list: Mastodon.List['id']
}
| {
page: 'Toot'
toot: Mastodon.Status['id']
}
| {
page: 'Account'
account: Mastodon.Account['id']
exclude_reblogs: boolean
only_media: boolean
}
| {
page: 'Toot'
toot: Mastodon.Status['id']
}
)
]
@ -209,22 +272,6 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
url: `timelines/list/${page.list}`,
params
})
case 'Toot':
const res1_1 = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${page.toot}`
})
const res2_1 = await apiInstance<{
ancestors: Mastodon.Status[]
descendants: Mastodon.Status[]
}>({
method: 'get',
url: `statuses/${page.toot}/context`
})
return {
body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants]
}
default:
return Promise.reject()
}
@ -454,4 +501,4 @@ const useTimelineMutation = ({
})
}
export { useTimelineQuery, useTimelineMutation }
export { useTootQuery, useTimelineQuery, useTimelineMutation }

View File

@ -1,3 +1,4 @@
import { InfiniteData } from '@tanstack/react-query'
import { PagedResponse } from '@utils/api/helpers'
export const infinitePageParams = {
@ -6,3 +7,6 @@ export const infinitePageParams = {
getNextPageParam: (lastPage: PagedResponse<any>) =>
lastPage.links?.next && { max_id: lastPage.links.next }
}
export const flattenPages = <T>(data: InfiniteData<PagedResponse<T[]>> | undefined): T[] | [] =>
data?.pages.map(page => page.body).flat() || []

View File

@ -89,9 +89,9 @@ const themeColors: {
},
border: {
light: 'rgba(25, 25, 25, 0.3)',
dark_lighter: 'rgba(255, 255, 255, 0.3)',
dark_darker: 'rgba(255, 255, 255, 0.3)'
light: 'rgb(180, 180, 180)',
dark_lighter: 'rgb(90, 90, 90)',
dark_darker: 'rgb(90, 90, 90)'
},
shimmerDefault: {