mirror of
https://github.com/tooot-app/app
synced 2025-02-18 12:50:46 +01:00
Notification working and end of list working
This commit is contained in:
parent
81a21d1d07
commit
2217d3eb5d
15
src/@types/mastodon.d.ts
vendored
15
src/@types/mastodon.d.ts
vendored
@ -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[]
|
||||
|
@ -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 => {
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
38
src/components/Timelines/Timeline/Shared/End.tsx
Normal file
38
src/components/Timelines/Timeline/Shared/End.tsx
Normal 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
|
@ -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',
|
||||
|
238
src/components/Timelines/Timeline/Shared/HeaderNotification.tsx
Normal file
238
src/components/Timelines/Timeline/Shared/HeaderNotification.tsx
Normal 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)
|
16
src/utils/fetches/relationshipFetch.ts
Normal file
16
src/utils/fetches/relationshipFetch.ts
Normal 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])
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user