1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Lots of updates

This commit is contained in:
Zhiyuan Zheng
2021-01-07 19:13:09 +01:00
parent dcb36a682d
commit 4b99813bb7
104 changed files with 2463 additions and 1619 deletions

View File

@ -23,6 +23,7 @@ export interface Props {
loading?: boolean
destructive?: boolean
disabled?: boolean
active?: boolean
strokeWidth?: number
size?: 'S' | 'M' | 'L'
@ -40,6 +41,7 @@ const Button: React.FC<Props> = ({
loading = false,
destructive = false,
disabled = false,
active = false,
strokeWidth,
size = 'M',
spacing = 'S',
@ -68,10 +70,29 @@ const Button: React.FC<Props> = ({
)
const colorContent = useMemo(() => {
if (overlay) {
return theme.primaryOverlay
if (active) {
return theme.blue
} else {
if (disabled) {
if (overlay) {
return theme.primaryOverlay
} else {
if (disabled) {
return theme.secondary
} else {
if (destructive) {
return theme.red
} else {
return theme.primary
}
}
}
}
}, [theme, disabled])
const colorBorder = useMemo(() => {
if (active) {
return theme.blue
} else {
if (disabled || loading) {
return theme.secondary
} else {
if (destructive) {
@ -81,7 +102,14 @@ const Button: React.FC<Props> = ({
}
}
}
}, [theme, disabled])
}, [theme, loading, disabled])
const colorBackground = useMemo(() => {
if (overlay) {
return theme.backgroundOverlay
} else {
return theme.background
}
}, [theme])
const children = useMemo(() => {
switch (type) {
@ -118,26 +146,7 @@ const Button: React.FC<Props> = ({
</>
)
}
}, [theme, content, loading, disabled])
const colorBorder = useMemo(() => {
if (disabled || loading) {
return theme.secondary
} else {
if (destructive) {
return theme.red
} else {
return theme.primary
}
}
}, [theme, loading, disabled])
const colorBackground = useMemo(() => {
if (overlay) {
return theme.backgroundOverlay
} else {
return theme.background
}
}, [theme])
}, [theme, content, loading, disabled, active])
enum spacingMapping {
XS = 'S',
@ -164,7 +173,7 @@ const Button: React.FC<Props> = ({
testID='base'
onPress={onPress}
children={children}
disabled={disabled || loading}
disabled={disabled || active || loading}
/>
</Animated.View>
)

View File

@ -1,4 +1,3 @@
import { StyleConstants } from '@root/utils/styles/constants'
import React, { createElement } from 'react'
import { StyleProp, View, ViewStyle } from 'react-native'
import * as FeatherIcon from 'react-native-feather'

258
src/components/Instance.tsx Normal file
View File

@ -0,0 +1,258 @@
import Button from '@components/Button'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native'
import hookApps from '@utils/queryHooks/apps'
import hookInstance from '@utils/queryHooks/instance'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { InstanceLocal, remoteUpdate } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { debounce } from 'lodash'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet, Text, TextInput, View } from 'react-native'
import { useQueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info'
import { toast } from './toast'
export interface Props {
type: 'local' | 'remote'
disableHeaderImage?: boolean
goBack?: boolean
}
const ComponentInstance: React.FC<Props> = ({
type,
disableHeaderImage,
goBack = false
}) => {
const navigation = useNavigation()
const dispatch = useDispatch()
const queryClient = useQueryClient()
const { t } = useTranslation('meRoot')
const { theme } = useTheme()
const [instanceDomain, setInstanceDomain] = useState<string | undefined>()
const [appData, setApplicationData] = useState<InstanceLocal['appData']>()
const instanceQuery = hookInstance({
instanceDomain,
options: { enabled: false, retry: false }
})
const applicationQuery = hookApps({
instanceDomain,
options: { enabled: false, retry: false }
})
useEffect(() => {
if (
applicationQuery.data?.client_id.length &&
applicationQuery.data?.client_secret.length
) {
setApplicationData({
clientId: applicationQuery.data.client_id,
clientSecret: applicationQuery.data.client_secret
})
}
}, [applicationQuery.data?.client_id])
const onChangeText = useCallback(
debounce(
text => {
setInstanceDomain(text.replace(/^http(s)?\:\/\//i, ''))
setApplicationData(undefined)
},
1000,
{
trailing: true
}
),
[]
)
useEffect(() => {
if (instanceDomain) {
instanceQuery.refetch()
}
}, [instanceDomain])
const processUpdate = useCallback(() => {
if (instanceDomain) {
haptics('Success')
switch (type) {
case 'local':
applicationQuery.refetch()
return
case 'remote':
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'RemotePublic' }
]
dispatch(remoteUpdate(instanceDomain))
queryClient.resetQueries(queryKey)
toast({ type: 'success', message: '重置成功' })
navigation.navigate('Screen-Remote-Root')
return
}
}
}, [instanceDomain])
const onSubmitEditing = useCallback(
({ nativeEvent: { text } }) => {
if (
text === instanceDomain &&
instanceQuery.isSuccess &&
instanceQuery.data &&
instanceQuery.data.uri
) {
processUpdate()
} else {
setInstanceDomain(text)
setApplicationData(undefined)
}
},
[instanceDomain, instanceQuery.isSuccess, instanceQuery.data]
)
const buttonContent = useMemo(() => {
switch (type) {
case 'local':
return t('content.login.button')
case 'remote':
return '登记'
}
}, [])
return (
<>
{!disableHeaderImage ? (
<View style={styles.imageContainer}>
<Image
source={require('assets/screens/meRoot/welcome.png')}
style={styles.image}
/>
</View>
) : null}
<View style={styles.base}>
<View style={styles.inputRow}>
<TextInput
style={[
styles.textInput,
{
color: theme.primary,
borderBottomColor: theme.border
}
]}
onChangeText={onChangeText}
autoCapitalize='none'
autoCorrect={false}
clearButtonMode='never'
keyboardType='url'
textContentType='URL'
onSubmitEditing={onSubmitEditing}
placeholder={t('content.login.server.placeholder')}
placeholderTextColor={theme.secondary}
returnKeyType='go'
/>
<Button
type='text'
content={buttonContent}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || applicationQuery.isFetching}
/>
</View>
<View>
<InstanceInfo
visible={instanceQuery.data?.title !== undefined}
header='实例名称'
content={instanceQuery.data?.title || undefined}
potentialWidth={10}
/>
<InstanceInfo
visible={instanceQuery.data?.short_description !== undefined}
header='实例介绍'
content={instanceQuery.data?.short_description || undefined}
potentialLines={5}
/>
<View style={styles.instanceStats}>
<InstanceInfo
style={{ alignItems: 'flex-start' }}
visible={instanceQuery.data?.stats?.user_count !== null}
header='用户总数'
content={
instanceQuery.data?.stats?.user_count?.toString() || undefined
}
potentialWidth={4}
/>
<InstanceInfo
style={{ alignItems: 'center' }}
visible={instanceQuery.data?.stats?.status_count !== null}
header='嘟嘟总数'
content={
instanceQuery.data?.stats?.status_count?.toString() || undefined
}
potentialWidth={4}
/>
<InstanceInfo
style={{ alignItems: 'flex-end' }}
visible={instanceQuery.data?.stats?.domain_count !== null}
header='嘟嘟总数'
content={
instanceQuery.data?.stats?.domain_count?.toString() || undefined
}
potentialWidth={4}
/>
</View>
<Text style={[styles.disclaimer, { color: theme.secondary }]}>
<Icon
name='Lock'
size={StyleConstants.Font.Size.M}
color={theme.secondary}
/>{' '}
</Text>
</View>
</View>
{type === 'local' && appData ? (
<InstanceAuth
instanceDomain={instanceDomain!}
appData={appData}
goBack={goBack}
/>
) : null}
</>
)
}
const styles = StyleSheet.create({
imageContainer: { flexDirection: 'row' },
image: { resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 },
base: {
marginVertical: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
},
inputRow: {
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
},
textInput: {
flex: 1,
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M
},
instanceStats: {
flex: 1,
flexDirection: 'row'
},
disclaimer: {
...StyleConstants.FontStyle.S,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginVertical: StyleConstants.Spacing.M
}
})
export default ComponentInstance

View File

@ -0,0 +1,74 @@
import { useNavigation } from '@react-navigation/native'
import { InstanceLocal, localAddInstance } from '@utils/slices/instancesSlice'
import * as AuthSession from 'expo-auth-session'
import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
export interface Props {
instanceDomain: string
appData: InstanceLocal['appData']
goBack?: boolean
}
const InstanceAuth = React.memo(
({ instanceDomain, appData, goBack }: Props) => {
const navigation = useNavigation()
const queryClient = useQueryClient()
const dispatch = useDispatch()
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri: 'exp://127.0.0.1:19000'
},
{
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
}
)
useEffect(() => {
;(async () => {
if (request?.clientId) {
await promptAsync()
}
})()
}, [request])
useEffect(() => {
;(async () => {
if (response?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri: 'exp://127.0.0.1:19000',
code: response.params.code,
extraParams: {
grant_type: 'authorization_code'
}
},
{
tokenEndpoint: `https://${instanceDomain}/oauth/token`
}
)
queryClient.clear()
dispatch(
localAddInstance({
url: instanceDomain,
token: accessToken,
appData
})
)
goBack && navigation.goBack()
}
})()
}, [response])
return <></>
},
() => true
)
export default InstanceAuth

View File

@ -0,0 +1,73 @@
import { ParseHTML } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React from 'react'
import { Dimensions, StyleSheet, Text, View, ViewStyle } from 'react-native'
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
export interface Props {
style?: ViewStyle
visible: boolean
header: string
content?: string
potentialWidth?: number
potentialLines?: number
}
const InstanceInfo = React.memo(
({
style,
visible,
header,
content,
potentialWidth,
potentialLines = 1
}: Props) => {
const { theme } = useTheme()
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
return (
<View style={[styles.base, style]}>
<Text style={[styles.header, { color: theme.primary }]}>{header}</Text>
<ShimmerPlaceholder
visible={visible}
stopAutoRun
width={
potentialWidth
? potentialWidth * StyleConstants.Font.Size.M
: Dimensions.get('screen').width -
StyleConstants.Spacing.Global.PagePadding * 4
}
height={StyleConstants.Font.LineHeight.M * potentialLines}
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
>
{content ? (
<ParseHTML
content={content}
size={'M'}
numberOfLines={5}
expandHint='介绍'
/>
) : null}
</ShimmerPlaceholder>
</View>
)
}
)
const styles = StyleSheet.create({
base: {
flex: 1,
marginTop: StyleConstants.Spacing.M,
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding
},
header: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS
}
})
export default InstanceInfo

View File

@ -134,7 +134,7 @@ const MenuRow: React.FC<Props> = ({
const styles = StyleSheet.create({
base: {
height: 50
minHeight: 50
},
core: {
flex: 1,

View File

@ -5,16 +5,12 @@ import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import React, { useCallback, useState } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import Animated, {
measure,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
withTiming
} from 'react-native-reanimated'
@ -40,11 +36,62 @@ const renderNode = ({
showFullLink: boolean
disableDetails: boolean
}) => {
if (node.name == 'a') {
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
switch (node.name) {
case 'a':
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
return (
<Text
key={index}
style={{
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
!disableDetails &&
navigation.push('Screen-Shared-Hashtag', {
hashtag: tag[1] || tag[2]
})
}}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
)
} else if (classes.includes('mention') && mentions) {
const accountIndex = mentions.findIndex(
mention => mention.url === href
)
return (
<Text
key={index}
style={{
color: accountIndex !== -1 ? theme.blue : undefined,
...StyleConstants.FontStyle[size]
}}
onPress={() => {
accountIndex !== -1 &&
!disableDetails &&
navigation.push('Screen-Shared-Account', {
account: mentions[accountIndex]
})
}}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
)
}
} else {
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
// Need example here
const content =
node.children && node.children[0] && node.children[0].data
const shouldBeTag =
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
return (
<Text
key={index}
@ -52,78 +99,31 @@ const renderNode = ({
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
!disableDetails &&
navigation.push('Screen-Shared-Hashtag', {
hashtag: tag[1] || tag[2]
})
}}
onPress={async () =>
!disableDetails && !shouldBeTag
? await openLink(href)
: navigation.push('Screen-Shared-Hashtag', {
hashtag: content.substring(1)
})
}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
)
} else if (classes.includes('mention') && mentions) {
const accountIndex = mentions.findIndex(mention => mention.url === href)
return (
<Text
key={index}
style={{
color: accountIndex !== -1 ? theme.blue : undefined,
...StyleConstants.FontStyle[size]
}}
onPress={() => {
accountIndex !== -1 &&
!disableDetails &&
navigation.push('Screen-Shared-Account', {
account: mentions[accountIndex]
})
}}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
{!shouldBeTag ? (
<Icon
color={theme.blue}
name='ExternalLink'
size={StyleConstants.Font.Size[size]}
/>
) : null}
{content || (showFullLink ? href : domain[1])}
</Text>
)
}
} else {
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
// Need example here
const content = node.children && node.children[0] && node.children[0].data
const shouldBeTag =
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
return (
<Text
key={index}
style={{
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
onPress={async () =>
!disableDetails && !shouldBeTag
? await openLink(href)
: navigation.push('Screen-Shared-Hashtag', {
hashtag: content.substring(1)
})
}
>
{!shouldBeTag ? (
<Icon
color={theme.blue}
name='ExternalLink'
size={StyleConstants.Font.Size[size]}
/>
) : null}
{content || (showFullLink ? href : domain[1])}
</Text>
)
}
} else {
if (node.name === 'p') {
break
case 'p':
if (!node.children.length) {
return <View key={index} /> // bug when the tag is empty
}
}
break
}
}

View File

@ -15,12 +15,12 @@ export interface Props {
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
const relationshipQueryKey = ['Relationship', { id }]
const queryClient = useQueryClient()
const fireMutation = useCallback(
({ type }: { type: 'authorize' | 'reject' }) => {
return client({
return client<Mastodon.Relationship>({
method: 'post',
instance: 'local',
url: `follow_requests/${id}/${type}`
@ -29,9 +29,9 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
[]
)
const mutation = useMutation(fireMutation, {
onSuccess: ({ body }) => {
onSuccess: res => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, body)
queryClient.setQueryData(relationshipQueryKey, res)
queryClient.refetchQueries(['Notifications'])
},
onError: (err: any, { type }) => {

View File

@ -2,10 +2,10 @@ import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { toast } from '@components/toast'
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
import hookRelationship from '@utils/queryHooks/relationship'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQuery, useQueryClient } from 'react-query'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
id: Mastodon.Account['id']
@ -14,13 +14,13 @@ export interface Props {
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
const query = useQuery(relationshipQueryKey, relationshipFetch)
const relationshipQueryKey = ['Relationship', { id }]
const query = hookRelationship({ id })
const queryClient = useQueryClient()
const fireMutation = useCallback(
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => {
return client({
return client<Mastodon.Relationship>({
method: 'post',
instance: 'local',
url: `accounts/${id}/${state ? 'un' : ''}${type}`
@ -29,9 +29,9 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
[]
)
const mutation = useMutation(fireMutation, {
onSuccess: ({ body }) => {
onSuccess: res => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, body)
queryClient.setQueryData(relationshipQueryKey, res)
},
onError: (err: any, { type }) => {
haptics('Error')

View File

@ -3,7 +3,7 @@ import Timeline from '@components/Timelines/Timeline'
import SegmentedControl from '@react-native-community/segmented-control'
import { useNavigation } from '@react-navigation/native'
import sharedScreens from '@screens/Shared/sharedScreens'
import { getLocalUrl, getRemoteUrl } from '@utils/slices/instancesSlice'
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
@ -21,7 +21,7 @@ export interface Props {
const Timelines: React.FC<Props> = ({ name, content }) => {
const navigation = useNavigation()
const { mode } = useTheme()
const localRegistered = useSelector(getLocalUrl)
const localActiveIndex = useSelector(getLocalActiveIndex)
const publicDomain = useSelector(getRemoteUrl)
const [segment, setSegment] = useState(0)
@ -30,7 +30,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
}, [])
const routes = content
.filter(p => (localRegistered ? true : p.page === 'RemotePublic'))
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
.map(p => ({ key: p.page }))
const renderScene = useCallback(
@ -42,12 +42,12 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
}
}) => {
return (
(localRegistered || route.key === 'RemotePublic') && (
(localActiveIndex !== null || route.key === 'RemotePublic') && (
<Timeline page={route.key} />
)
)
},
[localRegistered]
[localActiveIndex]
)
const screenComponent = useCallback(
@ -62,7 +62,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
initialLayout={{ width: Dimensions.get('window').width }}
/>
),
[segment, localRegistered]
[segment, localActiveIndex]
)
return (
@ -71,7 +71,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
name={`Screen-${name}-Root`}
options={{
headerTitle: name === 'Public' ? publicDomain : '',
...(localRegistered && {
...(localActiveIndex !== null && {
headerCenter: () => (
<View style={styles.segmentsContainer}>
<SegmentedControl

View File

@ -3,16 +3,17 @@ import TimelineConversation from '@components/Timelines/Timeline/Conversation'
import TimelineDefault from '@components/Timelines/Timeline/Default'
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@root/components/Timelines/Timeline/End'
import TimelineHeader from '@components/Timelines/Timeline/Header'
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
import { useScrollToTop } from '@react-navigation/native'
import { timelineFetch } from '@utils/fetches/timelineFetch'
import { updateNotification } from '@utils/slices/instancesSlice'
import { localUpdateNotification } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { RefreshControl, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { InfiniteData, useInfiniteQuery } from 'react-query'
import { InfiniteData } from 'react-query'
import { useDispatch } from 'react-redux'
import hookTimeline, { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export type TimelineData =
| InfiniteData<{
@ -41,15 +42,14 @@ const Timeline: React.FC<Props> = ({
disableRefresh = false,
disableInfinity = false
}) => {
const queryKey: QueryKey.Timeline = [
const queryKeyParams = {
page,
{
...(hashtag && { hashtag }),
...(list && { list }),
...(toot && { toot }),
...(account && { account })
}
]
...(hashtag && { hashtag }),
...(list && { list }),
...(toot && { toot }),
...(account && { account })
}
const queryKey: QueryKeyTimeline = ['Timeline', queryKeyParams]
const {
status,
data,
@ -61,24 +61,28 @@ const Timeline: React.FC<Props> = ({
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery(queryKey, timelineFetch, {
getPreviousPageParam: firstPage => {
return firstPage.toots.length
? {
direction: 'prev',
id: firstPage.toots[0].id
}
: undefined
},
getNextPageParam: lastPage => {
return lastPage.toots.length
? {
direction: 'next',
id: lastPage.toots[lastPage.toots.length - 1].id
}
: undefined
} = hookTimeline({
...queryKeyParams,
options: {
getPreviousPageParam: firstPage => {
return firstPage.toots.length
? {
direction: 'prev',
id: firstPage.toots[0].id
}
: undefined
},
getNextPageParam: lastPage => {
return lastPage.toots.length
? {
direction: 'next',
id: lastPage.toots[lastPage.toots.length - 1].id
}
: undefined
}
}
})
const flattenData = data?.pages ? data.pages.flatMap(d => [...d?.toots]) : []
const flattenPointer = data?.pages
? data.pages.flatMap(d => [d?.pointer])
@ -92,9 +96,9 @@ const Timeline: React.FC<Props> = ({
useEffect(() => {
if (page === 'Notifications' && flattenData.length) {
dispatch(
updateNotification({
localUpdateNotification({
unread: false,
latestTime: flattenData[0].created_at
latestTime: (flattenData[0] as Mastodon.Notification).created_at
})
)
}
@ -130,7 +134,7 @@ const Timeline: React.FC<Props> = ({
item={item}
queryKey={queryKey}
index={index}
{...(queryKey[0] === 'RemotePublic' && {
{...(queryKey[1].page === 'RemotePublic' && {
disableDetails: true,
disableOnPress: true
})}
@ -166,6 +170,7 @@ const Timeline: React.FC<Props> = ({
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
[isFetchingNextPage]
)
const ListHeaderComponent = useCallback(() => <TimelineHeader />, [])
const ListFooterComponent = useCallback(
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
[hasNextPage]
@ -207,6 +212,8 @@ const Timeline: React.FC<Props> = ({
ListEmptyComponent={flItemEmptyComponent}
{...(!disableRefresh && { refreshControl })}
ItemSeparatorComponent={ItemSeparatorComponent}
{...(queryKey &&
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
{...(toot && isSuccess && { onScrollToIndexFailed })}
maintainVisibleContentPosition={{
minIndexForVisible: 0,

View File

@ -10,27 +10,14 @@ import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
import client from '@root/api/client'
import { useMutation, useQueryClient } from 'react-query'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export interface Props {
conversation: Mastodon.Conversation
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
highlighted?: boolean
}
const fireMutation = async ({ id }: { id: Mastodon.Conversation['id'] }) => {
const res = await client({
method: 'post',
instance: 'local',
url: `conversations/${id}/read`
})
if (res.body.id === id) {
return Promise.resolve()
} else {
return Promise.reject()
}
}
const TimelineConversation: React.FC<Props> = ({
conversation,
queryKey,
@ -39,6 +26,13 @@ const TimelineConversation: React.FC<Props> = ({
const { theme } = useTheme()
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return client<Mastodon.Conversation>({
method: 'post',
instance: 'local',
url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
@ -49,7 +43,7 @@ const TimelineConversation: React.FC<Props> = ({
const onPress = useCallback(() => {
if (conversation.last_status) {
conversation.unread && mutate({ id: conversation.id })
conversation.unread && mutate()
navigation.push('Screen-Shared-Toot', {
toot: conversation.last_status
})

View File

@ -7,7 +7,8 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
import { getLocalAccountId } from '@utils/slices/instancesSlice'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
export interface Props {
item: Mastodon.Status
queryKey?: QueryKey.Timeline
queryKey?: QueryKeyTimeline
index: number
pinnedLength?: number
highlighted?: boolean
@ -33,7 +34,7 @@ const TimelineDefault: React.FC<Props> = ({
disableDetails = false,
disableOnPress = false
}) => {
const localAccountId = useSelector(getLocalAccountId)
const localAccount = useSelector(getLocalAccount)
const navigation = useNavigation()
let actualStatus = item.reblog ? item.reblog : item
@ -64,7 +65,7 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
status={actualStatus}
sameAccount={actualStatus.account.id === localAccountId}
sameAccount={actualStatus.account.id === localAccount?.id}
/>
</View>
@ -88,7 +89,7 @@ const TimelineDefault: React.FC<Props> = ({
queryKey={queryKey}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={actualStatus.account.id === localAccountId}
sameAccount={actualStatus.account.id === localAccount?.id}
/>
)}
{!disableDetails && actualStatus.media_attachments.length > 0 && (

View File

@ -0,0 +1,52 @@
import { useNavigation } from '@react-navigation/native'
import Icon from '@root/components/Icon'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
const TimelineHeader = React.memo(
() => {
const navigation = useNavigation()
const { theme } = useTheme()
return (
<View style={[styles.base, { borderColor: theme.border }]}>
<Text style={[styles.text, { color: theme.primary }]}>
{' '}
<Text
style={{ color: theme.blue }}
onPress={() =>
navigation.navigate('Screen-Me', {
screen: 'Screen-Me-Settings-UpdateRemote'
})
}
>
{' '}
<Icon
name='ArrowRight'
size={StyleConstants.Font.Size.S}
color={theme.blue}
/>
</Text>
</Text>
</View>
)
},
() => true
)
const styles = StyleSheet.create({
base: {
margin: StyleConstants.Spacing.Global.PagePadding,
paddingHorizontal: StyleConstants.Spacing.M,
paddingVertical: StyleConstants.Spacing.S,
borderWidth: 1,
borderRadius: 6
},
text: {
...StyleConstants.FontStyle.S
}
})
export default TimelineHeader

View File

@ -7,7 +7,8 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification'
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
import { getLocalAccountId } from '@utils/slices/instancesSlice'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
export interface Props {
notification: Mastodon.Notification
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
highlighted?: boolean
}
@ -24,7 +25,7 @@ const TimelineNotifications: React.FC<Props> = ({
queryKey,
highlighted = false
}) => {
const localAccountId = useSelector(getLocalAccountId)
const localAccount = useSelector(getLocalAccount)
const navigation = useNavigation()
const actualAccount = notification.status
? notification.status.account
@ -83,7 +84,7 @@ const TimelineNotifications: React.FC<Props> = ({
queryKey={queryKey}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === localAccountId}
sameAccount={notification.account.id === localAccount?.id}
/>
)}
{notification.status.media_attachments.length > 0 && (

View File

@ -4,6 +4,7 @@ import Icon from '@components/Icon'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast'
import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
@ -13,7 +14,7 @@ import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
status: Mastodon.Status
reblog: boolean
}
@ -36,7 +37,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
state?: boolean
}) => {
return client({
return client<Mastodon.Status>({
method: 'post',
instance: 'local',
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
@ -58,7 +59,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, [
queryKey[0] === 'Notifications'
queryKey[1].page === 'Notifications'
? 'status.id'
: reblog
? 'reblog.id'
@ -75,12 +76,12 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
if (pageIndex >= 0 && tootIndex >= 0) {
if (
(type === 'favourite' && queryKey[0] === 'Favourites') ||
(type === 'bookmark' && queryKey[0] === 'Bookmarks')
(type === 'favourite' && queryKey[1].page === 'Favourites') ||
(type === 'bookmark' && queryKey[1].page === 'Bookmarks')
) {
old!.pages[pageIndex].toots.splice(tootIndex, 1)
} else {
if (queryKey[0] === 'Notifications') {
if (queryKey[1].page === 'Notifications') {
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
typeof state === 'boolean' ? !state : true
} else {

View File

@ -3,9 +3,10 @@ import { Pressable, StyleSheet } from 'react-native'
import { Image } from 'react-native-expo-image-cache'
import { StyleConstants } from '@utils/styles/constants'
import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export interface Props {
queryKey?: QueryKey.Timeline
queryKey?: QueryKeyTimeline
account: Mastodon.Account
}

View File

@ -3,6 +3,7 @@ import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
@ -14,7 +15,7 @@ import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedCreated from './HeaderShared/Created'
export interface Props {
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
conversation: Mastodon.Conversation
}
@ -23,7 +24,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return client({
return client<Mastodon.Conversation>({
method: 'delete',
instance: 'local',
url: `conversations/${conversation.id}`

View File

@ -13,9 +13,10 @@ import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedVisibility from './HeaderShared/Visibility'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export interface Props {
queryKey?: QueryKey.Timeline
queryKey?: QueryKeyTimeline
status: Mastodon.Status
sameAccount: boolean
}

View File

@ -2,12 +2,13 @@ import client from '@api/client'
import haptics from '@components/haptics'
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
queryKey?: QueryKey.Timeline
queryKey?: QueryKeyTimeline
account: Pick<Mastodon.Account, 'id' | 'acct'>
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
}
@ -25,14 +26,14 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
switch (type) {
case 'mute':
case 'block':
return client({
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `accounts/${account.id}/${type}`
})
break
case 'reports':
return client({
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `reports`,

View File

@ -3,12 +3,13 @@ import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
domain: string
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
}
@ -21,7 +22,7 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
const { t } = useTranslation()
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return client({
return client<{}>({
method: 'post',
instance: 'local',
url: `domain_blocks`,

View File

@ -9,9 +9,10 @@ import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast'
import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export interface Props {
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
status: Mastodon.Status
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
}
@ -30,14 +31,14 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
switch (type) {
case 'mute':
case 'pin':
return client({
return client<Mastodon.Status>({
method: 'post',
instance: 'local',
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
}) // bug in response from Mastodon, but onMutate ignore the error in response
break
case 'delete':
return client({
return client<Mastodon.Status>({
method: 'delete',
instance: 'local',
url: `statuses/${status.id}`
@ -153,7 +154,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
),
style: 'destructive',
onPress: async () => {
await client({
await client<Mastodon.Status>({
method: 'delete',
instance: 'local',
url: `statuses/${status.id}`
@ -163,7 +164,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
setBottomSheetVisible(false)
navigation.navigate('Screen-Shared-Compose', {
type: 'edit',
incomingStatus: res.body
incomingStatus: res
})
})
.catch(() => {

View File

@ -6,6 +6,7 @@ import relativeTime from '@components/relativeTime'
import { TimelineData } from '@components/Timelines/Timeline'
import { ParseEmojis } from '@root/components/Parse'
import { toast } from '@root/components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
@ -15,7 +16,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
queryKey: QueryKey.Timeline
queryKey: QueryKeyTimeline
poll: NonNullable<Mastodon.Status['poll']>
reblog: boolean
sameAccount: boolean
@ -45,7 +46,7 @@ const TimelinePoll: React.FC<Props> = ({
}
})
return client({
return client<Mastodon.Poll>({
method: type === 'vote' ? 'post' : 'get',
instance: 'local',
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
@ -55,7 +56,7 @@ const TimelinePoll: React.FC<Props> = ({
[allOptions]
)
const mutation = useMutation(fireMutation, {
onSuccess: ({ body }) => {
onSuccess: (res) => {
queryClient.cancelQueries(queryKey)
queryClient.setQueryData<TimelineData>(queryKey, old => {
@ -75,9 +76,9 @@ const TimelinePoll: React.FC<Props> = ({
if (pageIndex >= 0 && tootIndex >= 0) {
if (reblog) {
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = body
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = res
} else {
old!.pages[pageIndex].toots[tootIndex].poll = body
old!.pages[pageIndex].toots[tootIndex].poll = res
}
}
return old