mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Reply working
This commit is contained in:
@ -50,6 +50,7 @@ const Timeline: React.FC<Props> = ({
|
||||
}
|
||||
]
|
||||
const {
|
||||
status,
|
||||
isSuccess,
|
||||
isLoading,
|
||||
isError,
|
||||
@ -109,14 +110,8 @@ const Timeline: React.FC<Props> = ({
|
||||
[]
|
||||
)
|
||||
const flItemEmptyComponent = useMemo(
|
||||
() => (
|
||||
<TimelineEmpty
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
[isLoading, isError]
|
||||
() => <TimelineEmpty status={status} refetch={refetch} />,
|
||||
[isLoading, isError, isSuccess]
|
||||
)
|
||||
const flOnRefresh = useCallback(
|
||||
() =>
|
||||
@ -124,7 +119,7 @@ const Timeline: React.FC<Props> = ({
|
||||
fetchMore(
|
||||
{
|
||||
direction: 'prev',
|
||||
id: flattenData[0].id
|
||||
id: flattenData.length ? flattenData[0].id : null
|
||||
},
|
||||
{ previous: true }
|
||||
),
|
||||
@ -142,11 +137,6 @@ const Timeline: React.FC<Props> = ({
|
||||
)
|
||||
const flFooter = useCallback(() => {
|
||||
return <TimelineEnd isFetchingMore={isFetchingMore} />
|
||||
// if (isFetchingMore) {
|
||||
// return <ActivityIndicator />
|
||||
// } else {
|
||||
// return <TimelineEnd />
|
||||
// }
|
||||
}, [isFetchingMore])
|
||||
const onScrollToIndexFailed = useCallback(error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@ -6,40 +6,61 @@ import TimelineAvatar from './Shared/Avatar'
|
||||
import TimelineHeaderConversation from './Shared/HeaderConversation'
|
||||
import TimelineContent from './Shared/Content'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import TimelineActions from './Shared/Actions'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Conversation
|
||||
queryKey: App.QueryKey
|
||||
highlighted?: boolean
|
||||
}
|
||||
// Unread and mark as unread
|
||||
const TimelineConversation: React.FC<Props> = ({ item }) => {
|
||||
const TimelineConversation: React.FC<Props> = ({
|
||||
item,
|
||||
queryKey,
|
||||
highlighted = false
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
const conversationOnPress = useCallback(
|
||||
() =>
|
||||
item.last_status &&
|
||||
navigation.navigate('Screen-Shared-Toot', {
|
||||
toot: item.last_status
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const conversationChildren = useMemo(() => {
|
||||
return item.last_status && <TimelineContent status={item.last_status} />
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.statusView}>
|
||||
<View style={styles.status}>
|
||||
<TimelineAvatar account={item.accounts[0]} />
|
||||
<View style={styles.details}>
|
||||
<TimelineHeaderConversation
|
||||
queryKey={queryKey}
|
||||
id={item.id}
|
||||
account={item.accounts[0]}
|
||||
created_at={item.last_status?.created_at}
|
||||
/>
|
||||
{/* Can pass toot info to next page to speed up performance */}
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
item.last_status &&
|
||||
navigation.navigate('Screen-Shared-Toot', {
|
||||
toot: item.last_status.id
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.last_status ? (
|
||||
<TimelineContent status={item.last_status} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Pressable>
|
||||
onPress={conversationOnPress}
|
||||
children={conversationChildren}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<TimelineActions queryKey={queryKey} status={item.last_status!} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -46,11 +46,14 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
const tootChildren = useMemo(
|
||||
() => (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
|
||||
}}
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
paddingLeft: highlighted
|
||||
? 0
|
||||
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
|
||||
}
|
||||
]}
|
||||
>
|
||||
{actualStatus.content.length > 0 && (
|
||||
<TimelineContent status={actualStatus} highlighted={highlighted} />
|
||||
@ -98,11 +101,10 @@ const styles = StyleSheet.create({
|
||||
header: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
flexDirection: 'row'
|
||||
},
|
||||
content: {
|
||||
paddingLeft: StyleConstants.Avatar.S + StyleConstants.Spacing.S
|
||||
paddingTop: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,35 +1,54 @@
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
import { QueryStatus } from 'react-query'
|
||||
import { ButtonRow } from 'src/components/Button'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
status: QueryStatus
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
const TimelineEmpty: React.FC<Props> = ({ isLoading, isError, refetch }) => {
|
||||
const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{isLoading && <ActivityIndicator />}
|
||||
{isError && (
|
||||
<>
|
||||
<Feather
|
||||
name='frown'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>加载错误</Text>
|
||||
<ButtonRow text='重试' onPress={() => refetch()} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
const children = useMemo(() => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return <ActivityIndicator />
|
||||
case 'error':
|
||||
return (
|
||||
<>
|
||||
<Feather
|
||||
name='frown'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
加载错误
|
||||
</Text>
|
||||
<ButtonRow text='重试' onPress={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<Feather
|
||||
name='smartphone'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
空无一物
|
||||
</Text>
|
||||
<ButtonRow text='刷新试试' onPress={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [status])
|
||||
return <View style={styles.base} children={children} />
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -92,7 +92,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
|
||||
navigation.navigate(getCurrentTab(navigation), {
|
||||
screen: 'Screen-Shared-Compose',
|
||||
params: {
|
||||
type: 'reply',
|
||||
type: status.visibility === 'direct' ? 'conversation' : 'reply',
|
||||
incomingStatus: status
|
||||
}
|
||||
})
|
||||
|
@ -1,65 +1,164 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import client from 'src/api/client'
|
||||
import { toast } from 'src/components/toast'
|
||||
|
||||
import relativeTime from 'src/utils/relativeTime'
|
||||
import { StyleConstants } from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import Emojis from './Emojis'
|
||||
|
||||
export interface Props {
|
||||
queryKey: App.QueryKey
|
||||
id: string
|
||||
account: Mastodon.Account
|
||||
created_at?: Mastodon.Status['created_at']
|
||||
}
|
||||
|
||||
const HeaderConversation: React.FC<Props> = ({ account, created_at }) => {
|
||||
const fireMutation = async ({ id }: { id: string }) => {
|
||||
const res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `conversations/${id}`
|
||||
})
|
||||
console.log(res)
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '删除私信成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '删除私信失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
const HeaderConversation: React.FC<Props> = ({
|
||||
queryKey,
|
||||
id,
|
||||
account,
|
||||
created_at
|
||||
}) => {
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
|
||||
queryCache.setQueryData(queryKey, old =>
|
||||
(old as {}[]).map((paging: any) => ({
|
||||
toots: paging.toots.filter((toot: any) => toot.id !== id),
|
||||
pointer: paging.pointer
|
||||
}))
|
||||
)
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
|
||||
const actionOnPress = useCallback(() => mutateAction({ id }), [])
|
||||
|
||||
const actionChildren = useMemo(
|
||||
() => (
|
||||
<Feather
|
||||
name='trash'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.M + 2}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndDate}>
|
||||
<View style={styles.name}>
|
||||
{account.emojis ? (
|
||||
<Emojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
size={14}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
fontBold={true}
|
||||
/>
|
||||
) : (
|
||||
<Text numberOfLines={1}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
|
||||
>
|
||||
{account.display_name || account.username}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{account.acct}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{created_at && (
|
||||
<View>
|
||||
<Text style={styles.created_at}>{relativeTime(created_at)}</Text>
|
||||
<View style={styles.meta}>
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{relativeTime(created_at)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.account} numberOfLines={1}>
|
||||
@{account.acct}
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={actionOnPress}
|
||||
children={actionChildren}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameAndDate: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
width: '80%'
|
||||
},
|
||||
name: {
|
||||
flexDirection: 'row',
|
||||
marginRight: 8,
|
||||
fontWeight: '900'
|
||||
},
|
||||
created_at: {
|
||||
fontSize: 12,
|
||||
lineHeight: 12,
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
marginRight: 8
|
||||
flexDirection: 'row'
|
||||
},
|
||||
account: {
|
||||
lineHeight: 14,
|
||||
flexShrink: 1
|
||||
flexShrink: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
},
|
||||
nameWithoutEmoji: {
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
},
|
||||
created_at: {
|
||||
fontSize: StyleConstants.Font.Size.S
|
||||
},
|
||||
action: {
|
||||
flexBasis: '20%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -61,7 +61,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndAction}>
|
||||
<View style={styles.nameAndMeta}>
|
||||
<View style={styles.name}>
|
||||
{emojis?.length ? (
|
||||
<Emojis
|
||||
@ -85,41 +85,41 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
{queryKey && (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={onPressAction}
|
||||
children={pressableAction}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.meta}>
|
||||
<View>
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
{status.visibility === 'private' && (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
style={styles.visibility}
|
||||
/>
|
||||
)}
|
||||
{status.application && status.application.name !== 'Web' && (
|
||||
<View style={styles.meta}>
|
||||
<View>
|
||||
<Text
|
||||
onPress={onPressApplication}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {status.application.name}
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{status.visibility === 'private' && (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
style={styles.visibility}
|
||||
/>
|
||||
)}
|
||||
{status.application && status.application.name !== 'Web' && (
|
||||
<View>
|
||||
<Text
|
||||
onPress={onPressApplication}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {status.application.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{queryKey && (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={onPressAction}
|
||||
children={pressableAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
{queryKey && (
|
||||
<BottomSheet
|
||||
visible={modalVisible}
|
||||
@ -157,29 +157,21 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flex: 1
|
||||
},
|
||||
nameAndAction: {
|
||||
flex: 1,
|
||||
flexBasis: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start'
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameAndMeta: {
|
||||
flexBasis: '80%'
|
||||
},
|
||||
name: {
|
||||
flexBasis: '85%',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameWithoutEmoji: {
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold
|
||||
},
|
||||
action: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
account: {
|
||||
flexShrink: 1,
|
||||
flex: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
},
|
||||
meta: {
|
||||
@ -197,6 +189,11 @@ const styles = StyleSheet.create({
|
||||
application: {
|
||||
fontSize: StyleConstants.Font.Size.S,
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
action: {
|
||||
flexBasis: '20%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -126,31 +126,29 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={{ flexBasis: '80%' }}>
|
||||
<View style={styles.nameAndAction}>
|
||||
<View style={styles.name}>
|
||||
{emojis?.length ? (
|
||||
<Emojis
|
||||
content={name}
|
||||
emojis={emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
fontBold={true}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.nameAndMeta}>
|
||||
<View style={styles.name}>
|
||||
{emojis?.length ? (
|
||||
<Emojis
|
||||
content={name}
|
||||
emojis={emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
fontBold={true}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
|
||||
>
|
||||
@{account}
|
||||
{name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.meta}>
|
||||
@ -180,6 +178,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{notification.type === 'follow' && (
|
||||
<View style={styles.relationship}>{relationshipIcon}</View>
|
||||
)}
|
||||
@ -192,22 +191,16 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameAndAction: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
nameAndMeta: {
|
||||
width: '80%'
|
||||
},
|
||||
name: {
|
||||
flexBasis: '90%',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameWithoutEmoji: {
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold
|
||||
},
|
||||
action: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
account: {
|
||||
flexShrink: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
|
Reference in New Issue
Block a user