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
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
9 changed files with 411 additions and 121 deletions

View File

@ -267,6 +267,21 @@ declare namespace Mastodon {
'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 = {
accounts?: Account[]
statuses?: Status[]

View File

@ -8,6 +8,7 @@ import TimelineConversation from 'src/components/Timelines/Timeline/Conversation
import { timelineFetch } from 'src/utils/fetches/timelineFetch'
import TimelineSeparator from './Timeline/Separator'
import TimelineEmpty from './Timeline/Empty'
import TimelineEnd from './Timeline/Shared/End'
export interface Props {
page: App.Pages
@ -44,10 +45,13 @@ const Timeline: React.FC<Props> = ({
isLoading,
isError,
isFetchingMore,
canFetchMore,
data,
fetchMore,
refetch
} = useInfiniteQuery(queryKey, timelineFetch)
} = useInfiniteQuery(queryKey, timelineFetch, {
getFetchMore: last => last?.toots.length > 0
})
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
@ -90,6 +94,7 @@ const Timeline: React.FC<Props> = ({
const flOnEndReach = useCallback(
() =>
!disableRefresh &&
canFetchMore &&
fetchMore({
direction: 'next',
id: flattenData[flattenData.length - 1].id
@ -100,7 +105,7 @@ const Timeline: React.FC<Props> = ({
if (isFetchingMore) {
return <ActivityIndicator />
} else {
return null
return <TimelineEnd />
}
}, [isFetchingMore])
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 { useNavigation } from '@react-navigation/native'
import TimelineAvatar from './Shared/Avatar'
import HeaderConversation from './Shared/HeaderConversation'
import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelineContent from './Shared/Content'
import { StyleConstants } from 'src/utils/styles/constants'
@ -14,45 +14,34 @@ export interface Props {
const TimelineConversation: React.FC<Props> = ({ item }) => {
const navigation = useNavigation()
const statusView = useMemo(() => {
return (
<View style={styles.statusView}>
<View style={styles.status}>
<TimelineAvatar uri={item.accounts[0].avatar} id={item.accounts[0].id} />
<View style={styles.details}>
<HeaderConversation
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
content={item.last_status.content}
emojis={item.last_status.emojis}
mentions={item.last_status.mentions}
spoiler_text={item.last_status.spoiler_text}
// tags={actualStatus.tags}
// style={{ flex: 1 }}
/>
) : (
<></>
)}
</Pressable>
</View>
return (
<View style={styles.statusView}>
<View style={styles.status}>
<TimelineAvatar account={item.accounts[0]} />
<View style={styles.details}>
<TimelineHeaderConversation
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>
</View>
</View>
)
}, [item])
return statusView
</View>
)
}
const styles = StyleSheet.create({

View File

@ -29,14 +29,14 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
StyleConstants.Avatar.S - // Avatar width
StyleConstants.Spacing.S // Avatar margin to the right
const pressableToot = useCallback(
const tootOnPress = useCallback(
() =>
navigation.navigate('Screen-Shared-Toot', {
toot: actualStatus
}),
[]
)
const childrenToot = useMemo(
const tootChildren = useMemo(
() => (
<>
{actualStatus.content.length > 0 && (
@ -63,8 +63,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
<TimelineAvatar account={actualStatus.account} />
<View style={styles.details}>
<TimelineHeaderDefault queryKey={queryKey} status={actualStatus} />
{/* Can pass toot info to next page to speed up performance */}
<Pressable onPress={pressableToot} children={childrenToot} />
<Pressable onPress={tootOnPress} children={tootChildren} />
<TimelineActions queryKey={queryKey} status={actualStatus} />
</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 { useNavigation } from '@react-navigation/native'
import Actioned from './Shared/Actioned'
import Avatar from './Shared/Avatar'
import HeaderDefault from './Shared/HeaderDefault'
import Content from './Shared/Content'
import Poll from './Shared/Poll'
import Attachment from './Shared/Attachment'
import Card from './Shared/Card'
import ActionsStatus from './Shared/Actions'
import TimelineActioned from './Shared/Actioned'
import TimelineActions from './Shared/Actions'
import TimelineAttachment from './Shared/Attachment'
import TimelineAvatar from './Shared/Avatar'
import TimelineCard from './Shared/Card'
import TimelineContent from './Shared/Content'
import TimelineHeaderNotification from './Shared/HeaderNotification'
import TimelinePoll from './Shared/Poll'
import { StyleConstants } from 'src/utils/styles/constants'
export interface Props {
@ -22,75 +23,63 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const actualAccount = notification.status
? notification.status.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(() => {
return (
<View style={styles.notificationView}>
<Actioned
action={notification.type}
name={
notification.account.display_name || notification.account.username
}
emojis={notification.account.emojis}
notification
/>
<View style={styles.notification}>
<Avatar uri={actualAccount.avatar} id={actualAccount.id} />
<View style={styles.details}>
<HeaderDefault
name={actualAccount.display_name || actualAccount.username}
emojis={actualAccount.emojis}
account={actualAccount.acct}
created_at={notification.created_at}
const tootOnPress = useCallback(
() =>
navigation.navigate('Screen-Shared-Toot', {
toot: notification
}),
[]
)
const tootChildren = useMemo(
() =>
notification.status ? (
<>
{notification.status.content.length > 0 && (
<TimelineContent status={notification.status} />
)}
{notification.status.poll && (
<TimelinePoll queryKey={queryKey} status={notification.status} />
)}
{notification.status.media_attachments.length > 0 && (
<TimelineAttachment
status={notification.status}
width={contentWidth}
/>
<Pressable
onPress={() =>
navigation.navigate('Screen-Shared-Toot', {
toot: notification.id
})
}
>
{notification.status ? (
<>
{notification.status.content && (
<Content
content={notification.status.content}
emojis={notification.status.emojis}
mentions={notification.status.mentions}
spoiler_text={notification.status.spoiler_text}
// tags={notification.notification.tags}
// style={{ flex: 1 }}
/>
)}
{notification.status.poll && (
<Poll poll={notification.status.poll} />
)}
{notification.status.media_attachments.length > 0 && (
<Attachment
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>
)}
{notification.status.card && (
<TimelineCard card={notification.status.card} />
)}
</>
) : null,
[notification.status?.poll?.voted]
)
return (
<View style={styles.notificationView}>
<TimelineActioned
action={notification.type}
account={notification.account}
notification
/>
<View style={styles.notification}>
<TimelineAvatar account={actualAccount} />
<View style={styles.details}>
<TimelineHeaderNotification notification={notification} />
<Pressable onPress={tootOnPress} children={tootChildren} />
{notification.status && (
<TimelineActions queryKey={queryKey} status={notification.status} />
)}
</View>
</View>
)
}, [notification])
return statusView
</View>
)
}
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 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 emojis = status.account.emojis
const account = status.account.acct
@ -173,7 +175,6 @@ const styles = StyleSheet.create({
account: {
flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS
// lineHeight: StyleConstants.Font.LineHeight.M
},
meta: {
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])
}