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:
@ -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>
|
||||
)
|
||||
|
@ -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
258
src/components/Instance.tsx
Normal 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
|
74
src/components/Instance/Auth.tsx
Normal file
74
src/components/Instance/Auth.tsx
Normal 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
|
73
src/components/Instance/Info.tsx
Normal file
73
src/components/Instance/Info.tsx
Normal 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
|
@ -134,7 +134,7 @@ const MenuRow: React.FC<Props> = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
height: 50
|
||||
minHeight: 50
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }) => {
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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 && (
|
||||
|
52
src/components/Timelines/Timeline/Header.tsx
Normal file
52
src/components/Timelines/Timeline/Header.tsx
Normal 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
|
@ -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 && (
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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}`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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`,
|
||||
|
@ -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`,
|
||||
|
@ -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(() => {
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user