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

Notification working and end of list working

This commit is contained in:
Zhiyuan Zheng
2020-12-12 18:22:22 +01:00
parent 81a21d1d07
commit 2217d3eb5d
9 changed files with 411 additions and 121 deletions

View File

@ -267,6 +267,21 @@ declare namespace Mastodon {
'reading:expand:spoilers'?: boolean 'reading:expand:spoilers'?: boolean
} }
type Relationship = {
id: string
following: boolean
showing_reblogs: boolean
followed_by: boolean
blocking: boolean
blocked_by: boolean
muting: boolean
muting_notifications: boolean
requested: boolean
domain_blocking: boolean
endorsed: boolean
note: string
}
type Results = { type Results = {
accounts?: Account[] accounts?: Account[]
statuses?: Status[] statuses?: Status[]

View File

@ -8,6 +8,7 @@ import TimelineConversation from 'src/components/Timelines/Timeline/Conversation
import { timelineFetch } from 'src/utils/fetches/timelineFetch' import { timelineFetch } from 'src/utils/fetches/timelineFetch'
import TimelineSeparator from './Timeline/Separator' import TimelineSeparator from './Timeline/Separator'
import TimelineEmpty from './Timeline/Empty' import TimelineEmpty from './Timeline/Empty'
import TimelineEnd from './Timeline/Shared/End'
export interface Props { export interface Props {
page: App.Pages page: App.Pages
@ -44,10 +45,13 @@ const Timeline: React.FC<Props> = ({
isLoading, isLoading,
isError, isError,
isFetchingMore, isFetchingMore,
canFetchMore,
data, data,
fetchMore, fetchMore,
refetch refetch
} = useInfiniteQuery(queryKey, timelineFetch) } = useInfiniteQuery(queryKey, timelineFetch, {
getFetchMore: last => last?.toots.length > 0
})
const flattenData = data ? data.flatMap(d => [...d?.toots]) : [] const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : [] const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
@ -90,6 +94,7 @@ const Timeline: React.FC<Props> = ({
const flOnEndReach = useCallback( const flOnEndReach = useCallback(
() => () =>
!disableRefresh && !disableRefresh &&
canFetchMore &&
fetchMore({ fetchMore({
direction: 'next', direction: 'next',
id: flattenData[flattenData.length - 1].id id: flattenData[flattenData.length - 1].id
@ -100,7 +105,7 @@ const Timeline: React.FC<Props> = ({
if (isFetchingMore) { if (isFetchingMore) {
return <ActivityIndicator /> return <ActivityIndicator />
} else { } else {
return null return <TimelineEnd />
} }
}, [isFetchingMore]) }, [isFetchingMore])
const onScrollToIndexFailed = useCallback(error => { const onScrollToIndexFailed = useCallback(error => {

View File

@ -1,9 +1,9 @@
import React, { useMemo } from 'react' import React from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import TimelineAvatar from './Shared/Avatar' import TimelineAvatar from './Shared/Avatar'
import HeaderConversation from './Shared/HeaderConversation' import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
@ -14,45 +14,34 @@ export interface Props {
const TimelineConversation: React.FC<Props> = ({ item }) => { const TimelineConversation: React.FC<Props> = ({ item }) => {
const navigation = useNavigation() const navigation = useNavigation()
const statusView = useMemo(() => { return (
return ( <View style={styles.statusView}>
<View style={styles.statusView}> <View style={styles.status}>
<View style={styles.status}> <TimelineAvatar account={item.accounts[0]} />
<TimelineAvatar uri={item.accounts[0].avatar} id={item.accounts[0].id} /> <View style={styles.details}>
<View style={styles.details}> <TimelineHeaderConversation
<HeaderConversation account={item.accounts[0]}
account={item.accounts[0]} created_at={item.last_status?.created_at}
created_at={item.last_status?.created_at} />
/> {/* Can pass toot info to next page to speed up performance */}
{/* Can pass toot info to next page to speed up performance */} <Pressable
<Pressable onPress={() =>
onPress={() => item.last_status &&
item.last_status && navigation.navigate('Screen-Shared-Toot', {
navigation.navigate('Screen-Shared-Toot', { toot: item.last_status.id
toot: item.last_status.id })
}) }
} >
> {item.last_status ? (
{item.last_status ? ( <TimelineContent status={item.last_status} />
<TimelineContent ) : (
content={item.last_status.content} <></>
emojis={item.last_status.emojis} )}
mentions={item.last_status.mentions} </Pressable>
spoiler_text={item.last_status.spoiler_text}
// tags={actualStatus.tags}
// style={{ flex: 1 }}
/>
) : (
<></>
)}
</Pressable>
</View>
</View> </View>
</View> </View>
) </View>
}, [item]) )
return statusView
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -29,14 +29,14 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
StyleConstants.Avatar.S - // Avatar width StyleConstants.Avatar.S - // Avatar width
StyleConstants.Spacing.S // Avatar margin to the right StyleConstants.Spacing.S // Avatar margin to the right
const pressableToot = useCallback( const tootOnPress = useCallback(
() => () =>
navigation.navigate('Screen-Shared-Toot', { navigation.navigate('Screen-Shared-Toot', {
toot: actualStatus toot: actualStatus
}), }),
[] []
) )
const childrenToot = useMemo( const tootChildren = useMemo(
() => ( () => (
<> <>
{actualStatus.content.length > 0 && ( {actualStatus.content.length > 0 && (
@ -63,8 +63,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
<TimelineAvatar account={actualStatus.account} /> <TimelineAvatar account={actualStatus.account} />
<View style={styles.details}> <View style={styles.details}>
<TimelineHeaderDefault queryKey={queryKey} status={actualStatus} /> <TimelineHeaderDefault queryKey={queryKey} status={actualStatus} />
{/* Can pass toot info to next page to speed up performance */} <Pressable onPress={tootOnPress} children={tootChildren} />
<Pressable onPress={pressableToot} children={childrenToot} />
<TimelineActions queryKey={queryKey} status={actualStatus} /> <TimelineActions queryKey={queryKey} status={actualStatus} />
</View> </View>
</View> </View>

View File

@ -1,15 +1,16 @@
import React, { useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { Dimensions, Pressable, StyleSheet, View } from 'react-native' import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Actioned from './Shared/Actioned' import TimelineActioned from './Shared/Actioned'
import Avatar from './Shared/Avatar' import TimelineActions from './Shared/Actions'
import HeaderDefault from './Shared/HeaderDefault' import TimelineAttachment from './Shared/Attachment'
import Content from './Shared/Content' import TimelineAvatar from './Shared/Avatar'
import Poll from './Shared/Poll' import TimelineCard from './Shared/Card'
import Attachment from './Shared/Attachment' import TimelineContent from './Shared/Content'
import Card from './Shared/Card' import TimelineHeaderNotification from './Shared/HeaderNotification'
import ActionsStatus from './Shared/Actions' import TimelinePoll from './Shared/Poll'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
@ -22,75 +23,63 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const actualAccount = notification.status const actualAccount = notification.status
? notification.status.account ? notification.status.account
: notification.account : notification.account
const contentWidth =
Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
StyleConstants.Avatar.S - // Avatar width
StyleConstants.Spacing.S // Avatar margin to the right
const statusView = useMemo(() => { const tootOnPress = useCallback(
return ( () =>
<View style={styles.notificationView}> navigation.navigate('Screen-Shared-Toot', {
<Actioned toot: notification
action={notification.type} }),
name={ []
notification.account.display_name || notification.account.username )
} const tootChildren = useMemo(
emojis={notification.account.emojis} () =>
notification notification.status ? (
/> <>
{notification.status.content.length > 0 && (
<View style={styles.notification}> <TimelineContent status={notification.status} />
<Avatar uri={actualAccount.avatar} id={actualAccount.id} /> )}
<View style={styles.details}> {notification.status.poll && (
<HeaderDefault <TimelinePoll queryKey={queryKey} status={notification.status} />
name={actualAccount.display_name || actualAccount.username} )}
emojis={actualAccount.emojis} {notification.status.media_attachments.length > 0 && (
account={actualAccount.acct} <TimelineAttachment
created_at={notification.created_at} status={notification.status}
width={contentWidth}
/> />
<Pressable )}
onPress={() => {notification.status.card && (
navigation.navigate('Screen-Shared-Toot', { <TimelineCard card={notification.status.card} />
toot: notification.id )}
}) </>
} ) : null,
> [notification.status?.poll?.voted]
{notification.status ? ( )
<>
{notification.status.content && ( return (
<Content <View style={styles.notificationView}>
content={notification.status.content} <TimelineActioned
emojis={notification.status.emojis} action={notification.type}
mentions={notification.status.mentions} account={notification.account}
spoiler_text={notification.status.spoiler_text} notification
// tags={notification.notification.tags} />
// style={{ flex: 1 }}
/> <View style={styles.notification}>
)} <TimelineAvatar account={actualAccount} />
{notification.status.poll && ( <View style={styles.details}>
<Poll poll={notification.status.poll} /> <TimelineHeaderNotification notification={notification} />
)} <Pressable onPress={tootOnPress} children={tootChildren} />
{notification.status.media_attachments.length > 0 && ( {notification.status && (
<Attachment <TimelineActions queryKey={queryKey} status={notification.status} />
media_attachments={notification.status.media_attachments} )}
sensitive={notification.status.sensitive}
width={Dimensions.get('window').width - 24 - 50 - 8}
/>
)}
{notification.status.card && (
<Card card={notification.status.card} />
)}
</>
) : (
<></>
)}
</Pressable>
{notification.status && (
<ActionsStatus queryKey={queryKey} status={notification.status} />
)}
</View>
</View> </View>
</View> </View>
) </View>
}, [notification]) )
return statusView
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -0,0 +1,38 @@
import { Feather } from '@expo/vector-icons'
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
const TimelineEnd: React.FC = () => {
const { theme } = useTheme()
return (
<View style={styles.base}>
<Text style={[styles.text, { color: theme.secondary }]}>
{' '}
<Feather
name='coffee'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
/>{' '}
</Text>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
padding: StyleConstants.Spacing.M
},
text: {
fontSize: StyleConstants.Font.Size.S,
marginLeft: StyleConstants.Spacing.S
}
})
export default TimelineEnd

View File

@ -20,7 +20,9 @@ export interface Props {
} }
const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => { const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
const domain = status.uri.split(new RegExp(/\/\/(.*?)\//))[1] const domain = status.uri
? status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
: ''
const name = status.account.display_name || status.account.username const name = status.account.display_name || status.account.username
const emojis = status.account.emojis const emojis = status.account.emojis
const account = status.account.acct const account = status.account.acct
@ -173,7 +175,6 @@ const styles = StyleSheet.create({
account: { account: {
flexShrink: 1, flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS
// lineHeight: StyleConstants.Font.LineHeight.M
}, },
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',

View File

@ -0,0 +1,238 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
ActivityIndicator,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
import Emojis from './Emojis'
import relativeTime from 'src/utils/relativeTime'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants'
import { useQuery } from 'react-query'
import { relationshipFetch } from 'src/utils/fetches/relationshipFetch'
import client from 'src/api/client'
import { toast } from 'src/components/toast'
export interface Props {
notification: Mastodon.Notification
}
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const name =
notification.account.display_name || notification.account.username
const emojis = notification.account.emojis
const account = notification.account.acct
const { theme } = useTheme()
const navigation = useNavigation()
const [since, setSince] = useState(relativeTime(notification.created_at))
const { status, data, refetch } = useQuery(
['Relationship', { id: notification.account.id }],
relationshipFetch,
{
enabled: false
}
)
const [updateData, setUpdateData] = useState<
Mastodon.Relationship | undefined
>()
useEffect(() => {
setTimeout(() => {
setSince(relativeTime(notification.created_at))
}, 1000)
}, [since])
const applicationOnPress = useCallback(() => {
navigation.navigate('Screen-Shared-Webview', {
uri: notification.status?.application!.website
})
}, [])
const relationshipOnPress = useCallback(() => {
client({
method: 'post',
instance: 'local',
url: `accounts/${notification.account.id}/${
updateData
? updateData.following || updateData.requested
? 'un'
: ''
: data.following || data.requested
? 'un'
: ''
}follow`
}).then(res => {
if (res.body.id === (updateData && updateData.id) || data.id) {
setUpdateData(res.body)
return Promise.resolve()
} else {
toast({ type: 'error', content: '请重试', autoHide: false })
return Promise.reject()
}
})
}, [data, updateData])
useEffect(() => {
if (notification.type === 'follow') {
refetch()
}
}, [notification.type])
const relationshipIcon = useMemo(() => {
switch (status) {
case 'idle':
case 'loading':
return <ActivityIndicator />
case 'success':
return (
<Pressable onPress={relationshipOnPress}>
<Feather
name={
updateData
? updateData.following
? 'user-check'
: updateData.requested
? 'loader'
: 'user-plus'
: data.following
? 'user-check'
: data.requested
? 'loader'
: 'user-plus'
}
color={
updateData
? updateData.following
? theme.primary
: theme.secondary
: data.following
? theme.primary
: theme.secondary
}
size={StyleConstants.Font.Size.M + 2}
/>
</Pressable>
)
default:
return null
}
}, [status, data, updateData])
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>
)}
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
>
@{account}
</Text>
</View>
</View>
<View style={styles.meta}>
<View>
<Text style={[styles.created_at, { color: theme.secondary }]}>
{since}
</Text>
</View>
{notification.status?.visibility === 'private' && (
<Feather
name='lock'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
style={styles.visibility}
/>
)}
{notification.status?.application &&
notification.status?.application.name !== 'Web' && (
<View>
<Text
onPress={applicationOnPress}
style={[styles.application, { color: theme.secondary }]}
>
- {notification.status?.application.name}
</Text>
</View>
)}
</View>
</View>
{notification.type === 'follow' && (
<View style={styles.relationship}>{relationshipIcon}</View>
)}
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row'
},
nameAndAction: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between'
},
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
},
meta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
},
created_at: {
fontSize: StyleConstants.Font.Size.S
},
visibility: {
marginLeft: StyleConstants.Spacing.S
},
application: {
fontSize: StyleConstants.Font.Size.S,
marginLeft: StyleConstants.Spacing.S
},
relationship: {
flexBasis: '20%',
flexDirection: 'row',
justifyContent: 'center'
}
})
export default React.memo(TimelineHeaderNotification, () => true)

View File

@ -0,0 +1,16 @@
import client from 'src/api/client'
export const relationshipFetch = async (
key: string,
{ id }: { id: string }
) => {
const res = await client({
method: 'get',
instance: 'local',
url: `accounts/relationships`,
params: {
'id[]': id
}
})
return Promise.resolve(res.body[0])
}